ListenAndServe函数
前面所有示例程序中,都在main函数中调用了ListenAndServe函数。下面对此函数所做的工作进行分析。该函数的实现为:
1
2
3 1func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe()
2}
3
该函数新建了一个Server对象,然后调用该Server的ListenAndServe方法并返回执行错误。
Server这个幕后大佬终于浮出水面了,基于net/http包建立的服务器程序都是它在操控的。让我们先看看该结构体的定义:
1
2
3
4
5
6
7
8
9 1type Server struct {
2 Addr string // TCP address to listen on, ":http" if empty
3 Handler Handler // handler to invoke, http.DefaultServeMux if nil
4 ReadTimeout time.Duration // maximum duration before timing out read of the request
5 WriteTimeout time.Duration // maximum duration before timing out write of the response
6 MaxHeaderBytes int // maximum size of request headers, DefaultMaxHeaderBytes if 0
7 TLSConfig *tls.Config // optional TLS config, used by ListenAndServeTLS
8 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 }
9
这里我们主要关心该结构体的Addr和Handler字段以及如下方法:
1
2 1func (srv *Server) ListenAndServe() errorfunc (srv *Server) Serve(l net.Listener) errorfunc (srv *Server) SetKeepAlivesEnabled(v bool)
2
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程序:
1
2
3
4 1package mainimport ( "net/http")func main() {
2 http.ListenAndServe(":8080", nil)
3}
4
这时访问http://localhost:8080/或其他任何路径并不是无法访问,而是得到前面提到的404 page not found。之所以能返回内容,正因为我们的服务器已经开始运行了,并且默认使用了DefaultServeMux这个Handler类型的变量。
路由
net/http包默认的路由功能
ServeMux是net/http包自带的HTTP请求多路复用器(路由器)。其定义为:
1
2
3
4
5 1type ServeMux struct {
2 mu sync.RWMutex
3 m map[string]muxEntry
4 hosts bool // whether any patterns contain hostnames}
5
ServeMux的方法都是我们前面见过的函数或类型:
1
2
3 1func (mux *ServeMux) Handle(pattern string, handler Handler)
2func (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)
3
每个ServeMux都包含一个映射列表,每个列表项主要将特定的URL模式与特定的Handler对应。为了方便,net/http包已经为我们定义了一个可导出的ServeMux类型的变量DefaultServeMux:
1
2 1var DefaultServeMux = NewServeMux()
2
如果我们决定使用ServeMux进行路由,则在大部分情况下,使用DefaultServeMux已经够了。net/http包包括一些使用DefaultServeMux的捷径:
-
调用http.Handle或http.HandleFunc实际上就是在往DefaultServeMux的映射列表中添加项目;
-
若ListenAndServe的第二个参数为nil,它也默认使用DefaultServeMux。
当然,如果我们不嫌麻烦,可不用这个DefaultServeMux,而是自己定义一个。前面方法1中的main函数实现的功能与以下代码是相同的:
1
2
3
4
5 1func main() { mux := http.NewServeMux()
2 mux.Handle("/view/", viewHandler{})
3 http.ListenAndServe(":8080", mux)
4}
5
当我们往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类似。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 1package mainimport ( "fmt" "io/ioutil" "net/http" "strings") type Page struct {
2 Title string
3 Body []byte}func loadPage(title string) (*Page, error) {
4 filename := title + ".txt"
5 body, err := ioutil.ReadFile(filename) if err != nil { return nil, err
6 } return &Page{Title: title, Body: body}, nil
7}func viewHandler() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, "/view/") {
8 fmt.Fprint(w, "404 page not found") return
9 }
10 title := r.URL.Path[len("/view/"):]
11 p, _ := loadPage(title)
12 fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
13 })
14}
15
16type myRouter struct {
17 m map[string]http.HandlerFunc
18}func NewRouter() *myRouter {
19 router := new(myRouter)
20 router.m = make(map[string]http.HandlerFunc) return router
21}
22
23func (router *myRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
24 host := strings.Split(r.Host, ":")[0] if f, ok := router.m[host]; ok {
25 f(w, r)
26 } else {
27 fmt.Fprint(w, "404 site not found")
28 }
29}
30
31func (router *myRouter) Add(host string, f http.HandlerFunc) {
32 router.m[host] = f
33}func main() {
34 router := NewRouter()
35 router.Add("localhost", viewHandler())
36 router.Add("127.0.0.1", viewHandler())
37 http.ListenAndServe(":8080", router)
38}
39
使用第三方路由包
以上自定义实现的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不太相同,让我们来随便看看其中一些中间件的函数签名:
1
2 1func 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
2
通常中间件实现的功能都是大多数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:使用全局变量共享状态
如在包的开头定义一个全局变量
1
2 1var db *sql.DB
2
前面讲到的在http包中定义的http.DefaultServeMux就是这样的全局变量。
这样我们自己编写的各个Handler就可以直接访问此全局变量了。对于第一类的与应用程序同生存周期的变量,这是一个好办法。但当我们的程序中有太多的Handler时,每个Handler可能都需要一些特别的全局变量,这时程序中可能有很多的全局变量,就会增加程序的耦合度,使维护变得困难。这时可以用结构体类型进一步封装这些全局变量,甚至把Handler定义为这种结构体的方法。
对于与请求同生存周期变量,也可以使用全局变量的方法在多个Handler之间共享状态。gorilla/context包就提供了这样一种功能。该包提供一种方法在一个全局变量中存储很多很多的东西,且可以线程安全地读写。该包中的一个全局变量可用来存储在一个请求生命周期内需要共享的东西。每次的请求是不同的,每次请求所要共享的状态也是不同的,为了实现最大限度的灵活性,该包差不多定义了一个具有以下类型的全局变量:
1
2 1map[*http.Request]map[string]interface{}
2
该全局变量针对每次请求存储一组状态的列表,在请求结束将该请求对应的状态映射列表清空。由于是用映射实现的,而映射并非线程安全的,因此在每次数据项改写操作过程中需要将其锁起来。
方法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接口,其定义如下:
1
2
3
4
5
6 1type Context interface {
2 Deadline() (deadline time.Time, ok bool)
3 Done() <-chan struct{}
4 Err() error Value(key interface{}) interface{}
5}
6
该接口的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函数的返回值就是树根:
1
2 1func Background() Context
2
该函数返回一个非nil但值为空的Context,该Context一般由main函数创建,是与进入请求对应的Context树的树根,它不能被取消、没有值、也没有过期时间。
有了树根,又该怎么创建根须呢?context包为我们提供了多个函数来创建根须:
1
2
3
4
5 1func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
2func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
3func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
4func WithValue(parent Context, key interface{}, val interface{}) Context
5
看见没有?这些函数都接收一个Context类型的参数parent,并返回一个Context类型的值,即表示是从接收的根部得到返回的须部。这些函数都是树形结构上创建根部的须部,须部是从复制根部得到的,并且根据接收参数设定须部的一些状态值,接着就可以将根须传递给下层的goroutine了。
让我们先来看看最后面的WithValue函数,它返回parent的一个副本,调用该副本的Value(key)方法将得到val。这样我们不光将根部原有的值保留了,还在须部中加入了新的值(若须部新加入值的key在根部已存在,则会覆盖根部的值)。
我们还不知道该怎么设置Context的过期时间,或直接撤销Context呢,答案就在前三个函数。先看第一个WithCancel函数,它只是将根部复制到须部,并且还返回一个额外的cancel CancelFunc函数类型变量,该函数类型的定义为:
1
2 1type CancelFunc func()
2
调用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