理解Go语言Web编程(下)
ListenAndServe
函数
前面所有示例程序中,都在main
函数中调用了ListenAndServe
函数。下面对此函数所做的工作进行分析。该函数的实现为:
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe()
}
该函数新建了一个Server
对象,然后调用该Server
的ListenAndServe
方法并返回执行错误。
Server
这个幕后大佬终于浮出水面了,基于net/http
包建立的服务器程序都是它在操控的。让我们先看看该结构体的定义:
type Server struct {
Addr string // TCP address to listen on, ":http" if empty
Handler Handler // handler to invoke, http.DefaultServeMux if nil
ReadTimeout time.Duration // maximum duration before timing out read of the request
WriteTimeout time.Duration // maximum duration before timing out write of the response
MaxHeaderBytes int // maximum size of request headers, DefaultMaxHeaderBytes if 0
TLSConfig *tls.Config // optional TLS config, used by ListenAndServeTLS
TLSNextProto map[string]func(*Server, *tls.Conn, Handler) ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
disableKeepAlives int32 // accessed atomically.
nextProtoOnce sync.Once // guards initialization of TLSNextProto in Serve
nextProtoErr error
}
这里我们主要关心该结构体的Addr
和Handler
字段以及如下方法:
func (srv *Server) ListenAndServe() errorfunc (srv *Server) Serve(l net.Listener) errorfunc (srv *Server) SetKeepAlivesEnabled(v bool)
ListenAndServe
在TCP网络地址srv.Addr
上监听接入连接,并通过Serve
方法处理连接。连接被接受后,则使TCP保持连接。如果srv.Addr
为空,则默认使用":http"
。ListenAndServe
返回的error
始终不为nil
。
Serve
在net.Listener
类型的l
上接受接入连接,为每个连接创建一个新的服务goroutine。该goroutine读请求并调用srv.Handler
以进行响应。同ListenAndServe
一样,Serve
返回的error
也一直不为nil
。
至此我们已经涉及到了涉及更底层网络I/O的net
包了,就不再继续深究了。
最简单的Web程序:
package mainimport ( "net/http")func main() {
http.ListenAndServe(":8080", nil)
}
这时访问http://localhost:8080/
或其他任何路径并不是无法访问,而是得到前面提到的404 page not found
。之所以能返回内容,正因为我们的服务器已经开始运行了,并且默认使用了DefaultServeMux
这个Handler
类型的变量。
路由
net/http
包默认的路由功能
ServeMux
是net/http
包自带的HTTP请求多路复用器(路由器)。其定义为:
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool // whether any patterns contain hostnames}
ServeMux
的方法都是我们前面见过的函数或类型:
func (mux *ServeMux) Handle(pattern string, handler Handler)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
每个ServeMux
都包含一个映射列表,每个列表项主要将特定的URL模式与特定的Handler
对应。为了方便,net/http
包已经为我们定义了一个可导出的ServeMux
类型的变量DefaultServeMux
:
var DefaultServeMux = NewServeMux()
如果我们决定使用ServeMux
进行路由,则在大部分情况下,使用DefaultServeMux
已经够了。net/http
包包括一些使用DefaultServeMux
的捷径:
- 调用
http.Handle
或http.HandleFunc
实际上就是在往DefaultServeMux
的映射列表中添加项目; - 若
ListenAndServe
的第二个参数为nil
,它也默认使用DefaultServeMux
。
当然,如果我们不嫌麻烦,可不用这个DefaultServeMux
,而是自己定义一个。前面方法1中的main
函数实现的功能与以下代码是相同的:
func main() { mux := http.NewServeMux()
mux.Handle("/view/", viewHandler{})
http.ListenAndServe(":8080", mux)
}
当我们往ServeMux
对象中填充足够的列表项后,并在ListenAndServe
函数中指定使用该路由器,则一旦HTTP请求进入,就会对该请求的一些部分(主要是URL
)进行检查,找出最匹配的Handler
对象以供调用,该对象可由Handler
方法获得。如果ServeMux
中已注册的任何URL模式都与接入的请求不匹配,Handler
方法的第一个返回值也非nil
,而是返回一个NotFoundHandler
,其正文正是404 page not found
,我们在前面已经见过它了。
ServeMux
同时也实现了Handler
接口。其ServeHTTP
方法完成了ServeMux
的主要功能,即根据HTTP请求找出最佳匹配的Handler
并执行之,它本身就是一个多Handler
封装器,是各个Handler
执行的总入口。这使我们可以像使用其他Handler
一样使用ServeMux
对象,如将其传入ListenAndServe
函数,真正地使我们的服务器按照ServeMux
给定的规则运行起来。
自定义路由实现
ServeMux
的路由功能是非常简单的,其只支持路径匹配,且匹配能力不强。许多时候Request.Method
字段是要重点检查的;有时我们还要检查Request.Host
和Request.Header
等字段。总之,在这些时候,ServeMux
已经变得不够用了,这时我们可以自己编写一个路由器。由于前面讲的Handle
或HandleFunc
函数默认都使用DefaultServeMux
,既然我们不再准备使用默认的路由器了,就不再使用这两个函数了。那么,只有向ListenAndServe
函数传入我们的路由器了。根据ListenAndServe
函数的签名,我们的路由器应首先是一个Handler
,现在的问题变成该如何编写此Handler
。很显然,此路由器Handler
不仅自身是一个Handler
,还需要能方便地将任务分配给其他Handler
,为此,它必须有类似Handle
或HandleFunc
这样的函数,只不过这样的函数变得更强大、更通用,或更适合我们的业务。
我们已经知道Handler
的实现有多种方法,现在我们需要考虑的是,我们的路由器应该是一个结构体还是一个函数。很显然,由于结构体具有额外的字段来存储其他信息,通常我们会希望我们的路由器是一个结构体,这样更利于功能的封装。以下程序实现了一个自定义的路由器myRouter
,该路由器的功能就是对请求的域名(主机名称)进行检查,必须是已经注册的域名(可以有多个)才能访问网站功能。这样如果不借助像Nginx这样的反向代理,也可以限定我们的网站只为特定域名服务,而当其他不相关的域名也指向本服务器IP地址后,通过该域名访问此服务器将返回一个404 site not found
页面。myRouter.Add
方法的功能其实与Handle
或HandleFunc
类似。
package mainimport (
"fmt"
"io/ioutil"
"net/http"
"strings")
type Page struct {
Title string
Body []byte}func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename) if err != nil { return nil, err
} return &Page{Title: title, Body: body}, nil
}func viewHandler() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, "/view/") {
fmt.Fprint(w, "404 page not found") return
}
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
})
}
type myRouter struct {
m map[string]http.HandlerFunc
}func NewRouter() *myRouter {
router := new(myRouter)
router.m = make(map[string]http.HandlerFunc) return router
}
func (router *myRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
host := strings.Split(r.Host, ":")[0] if f, ok := router.m[host]; ok {
f(w, r)
} else {
fmt.Fprint(w, "404 site not found")
}
}
func (router *myRouter) Add(host string, f http.HandlerFunc) {
router.m[host] = f
}func main() {
router := NewRouter()
router.Add("localhost", viewHandler())
router.Add("127.0.0.1", viewHandler())
http.ListenAndServe(":8080", router)
}
使用第三方路由包
以上自定义实现的myRouter
实在是太简陋了,它主要适用于一些简单的Web服务器程序(如当下比较流行的单页面Web程序)。当网站程序较复杂时,我们就需要一个功能强大的路由器了。在GitHub上已经有许多这样的路由器包了。如gorilla/mux就是其中一例。该包的使用与http.ServeMux
以及上面我们自己编写的myRouter
基本相同,不过功能要强大好多。
另外还有一些路由实现包,其使用方法http.ServeMux
稍有不同,如HttpRouter。该包重新定义了Handler
、Handle
和HandlerFunc
等类型或函数签名,因此要依照新的定义编写各种处理程序,所幸的是能有简单的方法继续使用原来的http.Handler
和http.HandlerFunc
。这里就不详细讲了。
中间件
什么是中间件
在前面路由器的实现中,我们已经意识到,通常只有尽量使用各种现成的包提供的功能,才能使我们编写Web服务器程序更加轻松。为了方便我们使用,这些现成的包通常以中间件的形式提供。所谓中间件,是指程序的一部分,它可以封装已有的程序功能,并且添加额外的功能。对于Go语言的Web编程来说,中间件就是在HTTP请求-响应处理链上的函数,他们是独立于我们的Web程序而编写,并能够访问我们的请求、响应以及其他需要共享的变量。在GitHub能找到许多Go语言写的HTTP中间件,这些中间件都以独立的包提供,这意味着他们是独立的,可以方便地添加到程序,或从中移除。
在上面的方法4中,我们在不经意间写出了一个中间件。这里的wrapperHandler
就是一个中间件,它就像一个喇叭外面的盒子,不仅将喇叭包起来成为一个音箱,还为音箱添加了电源开关、调节音量大小等功能。只要这个盒子的大小合适,它还可以用来包装其他的喇叭而构成不同的音箱。进一步地,我们甚至可以认为各种路由器(如我们前面写的myRouter
)其实也是中间件。
Go语言的中间件实现的要点:
- 中间件自身是一个
Handler
类型;或者是一个返回Handler
类型的函数;或是一个返回HandlerFunc
的函数;或者是返回一个函数,该函数的返回值为Handler
类型(真够绕的)。 - 中间件一般封装一个(或多个)
Handler
,并在适当的位置调用该Handler
,如通过调用f(w, r)
将w http.ResponseWriter, r *http.Request
两参数传递给被封装的Handler
并执行之。 - 在调用
Handler
之前或之后,可以实现自身的一些功能。 - 通过一定的机制在多个
Handler
之间共享状态。
gorilla/handlers包就提供了许多的中间件,他们的定义与上面的wrapperHandler
不太相同,让我们来随便看看其中一些中间件的函数签名:
func CanonicalHost(domain string, code int) func(h http.Handler) http.Handlerfunc CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handlerfunc CompressHandler(h http.Handler) http.Handler
通常中间件实现的功能都是大多数Web服务器程序共同需要的功能。如:
- 日志记录和追踪,显示调试信息;
- 连接或断开数据库连接;
- 提供静态文件HTTP服务;
- 验证请求信息,阻止恶意的或其他不想要的访问,限制访问频次;
- 写响应头,压缩HTTP响应,添加HSTS头;
- 从异常中恢复运行;
- 等等……
组合使用各种中间件
理解了中间件的概念以及其使用和编写方法之后,编写我们自己的Web服务器程序就不那么复杂了:无非就是编写各种各样的Handler
,并仔细设计将这些Handler
层层组合起来。当然这其中必然会涉及更多的知识,但那些都是细节了,我们这里并不进行讨论。
进一步的学习或应用可以结合已有的一些第三方中间件库来编写自己的程序,如Gorilla Web工具箱或codegangsta/negroni。这两者的共同特点就是遵照net/http
包的惯用法进行编程,只要理解了前面讲的知识,就能较轻易地理解这两者的原理和用法。这两者之中,codegangsta/negroni的聚合度要更高一点,它主动帮我们实现了一些常用功能。
当有人在社区中问究竟该使用哪个Go语言Web框架时,总会有人回答说使用net/http
包自身的功能就是不错的选择,这种回答实际上就是自己按照以上讲述的方法编写各种具体功能的Handler
,并使用网上已有的各种中间件,从而实现程序功能。现在看来,由于net/http
包以及Go语言的出色设计,这样的确能编写出灵活的且具有较大扩展性的程序,这种方法的确是一种不错的选择。但尽管如此,有时我们还是希望能有别人帮我们做更多的事情,甚至已经为我们规划好了程序的结构,这个时候,我们就要使用到框架。
在多个Handler
(或中间件)间共享状态
当我们的Web服务器程序的体量越来越大时,就必然有许许多多的Handler
(中间件也是Handler
);对于同一个请求,可能需要多个Handler
进行处理;多个Handler
被并列地或嵌套地调用。因此,这时就会涉及到多个Handler
之间共享状态(即共享变量)的问题。在前面我们已经见识过中间件的编写方式,就是提供各种方法将w http.ResponseWriter
和r *http.Request
参数先传递给中间件(封装器),然后再进一步传递给被封装的Handler
或HandlerFunc
,这里传递的w
和r
变量实际上就是被共享的状态。
通常,有两类变量需要在多个Handler
间共享。第一类是在服务器运行期间一直存在,且被多个Handler
共同使用的变量,如一个数据库连接,存储session所用的仓库,甚至前面讲的ServeMux
中存储pattern
和Handler
间对应关系的列表等,我们将第一类变量称作“与应用程序同生存周期的变量”。第二类是只在单个请求的处理期间存在的变量,如从Request
信息中得出的用户ID和授权码等,我们将第二类变量称作“与请求同生存周期变量”,对于不同的请求,需要的这种变量的类型、个数都不固定。
另外,在Go语言中,每次请求处理都需要启动一个独立的goroutine,这时在Handler
间共享状态还不涉及线程安全问题;但有些请求的处理过程中可能会启动更多的goroutine,如某个处理请求的goroutine中,再启动一个goroutine进行RPC,这时在多个Handler
间共享状态时,要确保该变量是线程安全的,即不能在某个goroutine修改某个变量的同时,另外一个goroutine在读此变量。如果将同一个变量传递给多个goroutine,一旦该变量被修改或设为不可用,这种改变对所有goroutine应该是一致的。当编写Web程序时,常常遇到与请求同生存周期变量,我们往往无法精确预料需要保存的变量类型和变量个数,这时最方便的是使用映射类型进行保存,而映射又不是线程安全的。因此,必须采取措施保证被传递的变量是线程安全的。
在多个Handler
间传递变量的方法可归结为两种:
方法a:使用全局变量共享状态
如在包的开头定义一个全局变量
var db *sql.DB
前面讲到的在http
包中定义的http.DefaultServeMux
就是这样的全局变量。
这样我们自己编写的各个Handler
就可以直接访问此全局变量了。对于第一类的与应用程序同生存周期的变量,这是一个好办法。但当我们的程序中有太多的Handler
时,每个Handler
可能都需要一些特别的全局变量,这时程序中可能有很多的全局变量,就会增加程序的耦合度,使维护变得困难。这时可以用结构体类型进一步封装这些全局变量,甚至把Handler
定义为这种结构体的方法。
对于与请求同生存周期变量,也可以使用全局变量的方法在多个Handler
之间共享状态。gorilla/context包就提供了这样一种功能。该包提供一种方法在一个全局变量中存储很多很多的东西,且可以线程安全地读写。该包中的一个全局变量可用来存储在一个请求生命周期内需要共享的东西。每次的请求是不同的,每次请求所要共享的状态也是不同的,为了实现最大限度的灵活性,该包差不多定义了一个具有以下类型的全局变量:
map[*http.Request]map[string]interface{}
该全局变量针对每次请求存储一组状态的列表,在请求结束将该请求对应的状态映射列表清空。由于是用映射实现的,而映射并非线程安全的,因此在每次数据项改写操作过程中需要将其锁起来。
方法b:修改Handler
的定义通过传递参数共享状态
既然w http.ResponseWriter
和r *http.Request
就是在各个Handler
之间共享的两个状态变量,那能不能修改http
包,以同样的方法共享更多的状态变量?当然能,并且还有多种方法:
示例1:修改Handler
接口的ServeHTTP
函数签名,使其接受一个额外的参数。如使其变为ServeHTTP(http.ResponseWriter, *http.Request, int)
,从而可额外将一个int
类型变量(如用户ID)传递给Handler
。
示例2:修改Request
,使其包含需要共享的额外的字段。
示例3:设计一个类型,使它既包含Request
的内容,又实现了ResponseWriter
接口,同时又可包含额外的变量。
还有更多种方法,既然不再必须遵守http
包中关于Handler
实现的约定,我们可以随心所欲地编写我们的Handler
。这种方法对于与请求同生存周期变量的共享非常有用。已经存在着许许多多的Go语言Web框架,往往每种框架都规定了一种编写Handler
的方法,都能更方便地在各个Handler
之间共享状态。我们似乎获得了更大的自由,但请注意,这样一来,我们往往需要修改http
包中的许多东西,并且不使用惯用的方法来编写Handler
或中间件,使得各个Handler
或中间件对不同的框架是不通用的。因此,这些为了更好地实现在多个Handler
间共享状态的方法,反倒使Go语言的Web编程世界变得支离破碎。
还需要说明一点。我们提倡编写标准的Handler
来使我们的代码更容易调用第三方中间件或被第三方中间件调用,但并不意味着在编程时,所有的处理函数或类型都要编写成Handler
形式,因为这样反而会限制了我们的自由。只要我们的函数或类型不是可导出的,并且不与其他中间件交互,我们就可以随意地编写他们。这样一来,函数或方法就可以随意地定义,共享状态并不是那么难。
通过上下文(context)共享状态
Context通常被译作上下文或语境,它是一个比较抽象的概念,可以将其理解为程序单元的一个运行状态(或快照)。这里的程序单元可以为一个goroutine,或为一个Handler
。如每个goroutine在执行之前,都要先知道整个程序当前的执行状态,通常将这些执行状态封装在一个ctx
(context的缩写)结构体变量中,传递给要执行的goroutine中。上下文的概念几乎已经成为传递与请求同生存周期变量的标准方法,这时ctx
不光要在多个Handler
之间传递,同时也可能在多个goroutine之间传递,因此我们必须保证所传递的ctx
变量是类型安全的。
所幸的是,已经存在一种成熟的机制在多个goroutine间线程安全地传递变量了,具体请参见Go Concurrency Patterns: Context,golang.org/x/net/context包就是这种机制的实现。context
包不仅实现了在程序单元(goroutine、API边界等)之间共享状态变量的方法,同时能通过简单的方法,使我们在被调用程序单元的外部,通过设置ctx
变量值,将过期或撤销这些信号传递给被调用的程序单元。
在Go 1.7中,context
可能作为最顶层的包进入标准库。context
包能被应用于多种场合,但最主要的场合应该是在多个goroutine间(其实也是在多个Handler
间)方便、安全地共享状态。为此,在Go 1.7中,随着context
包的引入,将会在http.Request
结构体中添加一个新的字段Context
。这种方法正是前面方法b中的示例2所做的,这样一来,我们就定义了一种在多个Handler
间共享状态的标准方法,有可能使Go语言已经开始变得破碎的Web编程世界得以弥合。
既然context
包这么重要,让我们来了解一下它吧。context
包的核心就是Context
接口,其定义如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error Value(key interface{}) interface{}
}
该接口的Value
方法返回与一个key
(不存在key
时就用nil
)对应的值,该值就是ctx
要传递的具体变量值。除此之外,我们定义了专门的方法来额外地标明某个Context
是否已关闭(超过截止时间或被主动撤销)、关闭的时间及原因:Done
方法返回一个信道(channel),当Context
被撤销或过期时,该信道是关闭的,即它是一个表示Context
是否已关闭的信号;当Done
信道关闭后,Err
方法表明Context
被撤的原因;当Context
将要被撤销时,Deadline
返回撤销执行的时间。在Web编程时,Context
对象总是与一个请求对应的,若Context
已关闭,则与该请求相关联的所有goroutine应立即释放资源并退出。
似乎Context
接口没有提供方法来设置其值和过期时间,也没有提供方法直接将其自身撤销。也就是说,Context
不能改变和撤销其自身。那么该怎么通过Context
传递改变后的状态呢?请继续读下去吧。
无论是goroutine,他们的创建和调用关系总是像一棵树的根系一样层层进行的,更靠根部的goroutine应有办法主动关闭其下属的goroutine的执行(不然程序可能就失控了)。为了实现这种关系,我们的Context
结构也应该像一棵树的根系,根须总是由根部衍生出来的。要创建Context
树,第一步就是要得到树根,context.Background
函数的返回值就是树根:
func Background() Context
该函数返回一个非nil但值为空的Context
,该Context
一般由main
函数创建,是与进入请求对应的Context
树的树根,它不能被取消、没有值、也没有过期时间。
有了树根,又该怎么创建根须呢?context
包为我们提供了多个函数来创建根须:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context
看见没有?这些函数都接收一个Context
类型的参数parent
,并返回一个Context
类型的值,即表示是从接收的根部得到返回的须部。这些函数都是树形结构上创建根部的须部,须部是从复制根部得到的,并且根据接收参数设定须部的一些状态值,接着就可以将根须传递给下层的goroutine了。
让我们先来看看最后面的WithValue
函数,它返回parent
的一个副本,调用该副本的Value(key)
方法将得到val
。这样我们不光将根部原有的值保留了,还在须部中加入了新的值(若须部新加入值的key
在根部已存在,则会覆盖根部的值)。
我们还不知道该怎么设置Context
的过期时间,或直接撤销Context
呢,答案就在前三个函数。先看第一个WithCancel
函数,它只是将根部复制到须部,并且还返回一个额外的cancel CancelFunc
函数类型变量,该函数类型的定义为:
type CancelFunc func()
调用CancelFunc
对象将撤销对应的Context
对象,这就是主动撤销Context
的方法。也就是说,在根部Context
所对应的环境中,通过WithCancel
函数不仅可创建须部的Context
,同时也获得了该须部Context
的一个命门机关,只要一触发该机关,该须部Context
(以及须部的须部)都将一命呜呼。
WithDeadline
函数的作用也差不多,它返回的Context
类型值同样是parent
的副本,但其过期时间由deadline
和parent
的过期时间共同决定。当parent
的过期时间早于传入的deadline
时间时,返回的根须过期时间应与parent
相同(根部过期时,其所有的根须必须同时关闭);反之,返回的根须的过期时间则为deadline
。WithTimeout
函数又和WithDeadline
类似,只不过它传入的是从现在开始Context
剩余的生命时长。WithDeadline
和WithTimeout
同样也都返回了所创建的子Context
的命门机关:一个CancelFunc
类型的函数变量。
context
包实现的功能使得根部Context
所处的环境总是对须部Context
有生杀予夺的大权。这样一来,我们的根部goroutine对须部的goroutine也就有了控制权。
概括来说,在请求处理时,上下文具有如下特点:
-
Context
对象(ctx
变量)的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个ctx
变量(它为Context
树结构的树根);在请求处理结束后,撤销此ctx
变量,释放资源。 - 每次创建一个goroutine或调用一个
Handler
,要么将原有的ctx
传递给goroutine,要么创建ctx
的一个子Context
并传递给goroutine。 - 为了使多个中间件相互链式调用,必须以标准的方法在多个
Handler
之间传递ctx
变量。如重新规定Handler
接口中ServeHTTP
方法的签名为ServeHTTP(context.Context, http.ResponseWriter, *http.Request)
,或将Context
作为Request
结构体的一个字段。 -
ctx
对象能灵活地存储不同类型、不同数目的值,并且使多个goroutine安全地读写其中的值。 - 当通过父
Context
对象创建子Context
对象时,可同时获得子Context
的一个撤销函数,这样父Context
对象的创建环境就获得了对子Context
将要被传递到的goroutine的撤销权。 - 在子
Context
被传递到的goroutine中,应该对该子Context
的Done
信道(channel)进行监控,一旦该信道被关闭(即上层运行环境撤销了本goroutine的执行),应主动终止对当前请求信息的处理,释放资源并返回。
现在,是时候给出点示例代码来看看context
包具体该如何应用了。但由于篇幅所限,加之短短几行代码难以说明白context
包的用法,这里并不准备进行举例。Go Concurrency Patterns: Context一文中所列举的“Google Web Search”示例则是一个极好的学习示例,请自行移步去看吧。
框架
我们在前面已经费劲口舌地说明了当用Go写Web服务器程序时,该如何实现路由功能,以及该如何用规范的方式编写Handler
(或中间件)。但一个Web程序的编写往往要涉及更多的方面,我们在前面介绍中间件时已经说过,各种各样的中间件能够帮助我们完成这些任务。但许多时候,我们总是希望他人帮我们完成更多的事情,从而使我们自己的工作更加省力。应运这种需求,就产生了许许多多的Web框架。根据架构的不同,这些框架大致可分为两大类:
第一类是微架构型框架。其核心框架只提供很少的功能,而更多的功能则需要组合各种中间件来提供,因此这种框架也可称为混搭型框架。它相当灵活,但相对来说需要使用者在组合使用各种中间件时花费更大的力气。像Echo、Goji、Gin等都属于微架构型框架。
第二类是全能型架构。它基本上提供了你编写Web应用时需要的所有功能,因此更加重型,多数使用MVC架构模式设计。在使用这类框架时你可能感觉更轻省,但其做事风格一般不同于Go语言惯用的风格,你也较难弄明白这些框架是如何工作的。像Beego、Revel等就属于全能型架构。
对于究竟该选择微架构还是全能型架构,仍有较多的争议。像The Case for Go Web Frameworks一文就力挺全能型架构,并且其副标题就是“Idiomatic Go is not a religion”,但该文也收到了较多的反对意见,见这里和这里。总体上来说,Go语言社区已越来越偏向使用微架构型框架,当将来context
包进入标准库后,http.Handler
本身就定义了较完善的中间件编写规范,这种使用微架构的趋势可能更加明显,并且各种微架构的实现方式有望进一步走向统一,这样其实http
包就是一个具有庞大生态系统的微架构框架。
更加自我
在此之前,我们一直在谈论net/http
包,但实际上我们甚至可以完全不用此包而编写Web服务器程序。如有人编写了fasthttp包,并声称它比net/http
包快10倍,并且前面提到的Echo框架也可以在底层使用此包。听起来或许很好,但这样一来,我们编写Handler
和中间件的方式就会大变了,最终可能置我们于孤独的境地。
这里之所以介绍fasthttp包,只是为了告诉大家,我们总有更多的选择,千万不要把思维局限在某种方法或某个框架。随着我们对自身需求把握得更加准确,以及对程序质量要求的提高,我们可能真的会去考虑这些选择,而到那时,则必须对Go语言Web编程有更深刻的理解。
参考文章
- Writing HTTP Middleware in Go
- The http.HandlerFunc wrapper technique in #golang
- Building Web Apps with Go
- Build Web Application with Golang
- Custom variables in http.Request
- Go Concurrency Patterns: Context
- Go’s net/context and http.Handler
- Context of incoming request
来源:http://www.chingli.com/coding/understanding-go-web-app/
- 通过shell脚本同时监控多个数据库负载(r5笔记第14天)
- Java 定时器 Timer 的使用.
- 通过shell脚本来统计段大小(r5笔记第14天)
- Linux下配置MySQL主从复制(r5笔记第13天)
- Final 关键字
- ArrayList 和 LinkedList的执行效率比较
- 关于consistent gets(r5笔记第12天)
- wait/notify 实现多线程交叉备份
- 01.SVN介绍与安装
- 由sqlplus中的一个小细节所做的折腾(r5笔记第11天)
- 浅析多线程的对象锁和Class锁
- 使用strace诊断奇怪的sqlplus登录问题(r5笔记第29天)
- 读书笔记 之《Thinking in Java》(对象、集合、异常)
- 深度解析dba_segments和sys.seg$中的细节差异(上) (r5笔记第27天)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法