断断续续学Go语言很久了,一直没有涉及Web编程方面的东西。因为仅是凭兴趣去学习的,时间有限,每次去学,也只是弄个一知半解。不过这两天下定决心把Go语言Web编程弄懂,就查了大量资料,边学边记博客。希望我的这个学习笔记对其他人同样有帮助,由于只是业余半吊子学习,文中必然存在诸多不当之处,恳请读者留言指出,在此先道一声感谢!
本文只是从原理方面对Go的Web编程进行理解,尤其是详细地解析了net/http包。由于篇幅有限,假设读者已经熟悉Writing Web Applications这篇文章,这里所进行的工作只是对此文中只是的进一步深入学习和扩充。
Go语言Web程序的实质
利用Go语言构建Web应用程序,实质上是构建HTTP服务器。HTTP是一个简单的请求-响应协议,通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。下图为最简化的HTTP协议处理流程。
HTTP请求和响应流程
从上图可知,构建在服务器端运行的Web程序的基本要素包括:
-
如何分析和表示HTTP请求;
-
如何根据HTTP请求以及程序逻辑生成HTTP响应(包括生成HTML网页);
-
如何使服务器端一直正确地运行以接受请求并生成响应。
Go语言有关Web程序的构建主要涉及net/http包,因此这里所给的各种函数、类型、变量等标识符,除了特别说明外,都是属于net/http包内的。
请求和响应信息的表示
HTTP 1.1中,请求和响应信息都是由以下四个部分组成,两者之间格式的区别是开始行不同。
开始行。位于第一行。在请求信息中叫
请求行,在响应信息中叫
状态行。
-
请求行:构成为请求方法 URI 协议/版本,例如GET /images/logo.gif HTTP/1.1;
- 响应行:构成为协议版本 状态代码 状态描述,例如HTTP/1.1 200 OK。
-
头。零行或多行。包含一些额外的信息,用来说明浏览器、服务器以及后续正文的一些信息。
-
空行。
-
正文。包含客户端提交或服务器返回的一些信息。请求信息和响应信息中都可以没有此部分。
开始行和头的各行必须以<CR><LF>作为结尾。空行内必须只有<CR><LF>而无其他空格。在HTTP/1.1协议中,开始行和头都是以ASCII编码的纯文本,所有的请求头,除Host外,都是可选的。
HTTP请求信息由客户端发来,Web程序要做的首先就是分析这些请求信息,并用Go语言中响应的数据对象来表示。在net/http包中,用Request结构体表示HTTP请求信息。其定义为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 1type Request struct {
2 Method string
3 URL *url.URL
4 Proto string // "HTTP/1.0"
5 ProtoMajor int // 1
6 ProtoMinor int // 0
7 Header Header
8 Body io.ReadCloser
9 ContentLength int64
10 TransferEncoding []string
11 Close bool
12 Host string
13 Form url.Values
14 PostForm url.Values
15 MultipartForm *multipart.Form
16 Trailer Header
17 RemoteAddr string
18 RequestURI string
19 TLS *tls.ConnectionState
20 Cancel <-chan struct{}
21}
22
当收到并
理解(将请求信息解析为Request类型变量)了请求信息之后,就需要根据相应的处理逻辑,构建响应信息。net/http包中,用Response结构体表示响应信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1type Response struct {
2 Status string // e.g. "200 OK"
3 StatusCode int // e.g. 200
4 Proto string // e.g. "HTTP/1.0"
5 ProtoMajor int // e.g. 1
6 ProtoMinor int // e.g. 0
7 Header Header
8 Body io.ReadCloser
9 ContentLength int64
10 TransferEncoding []string
11 Close bool
12 Trailer Header
13 Request *Request
14 TLS *tls.ConnectionState
15}
16
如何构建响应信息
很显然,前面给出的Request和Response结构体都相当复杂。好在客户端发来的请求信息是符合HTTP协议的,因此net/http包已经能够根据请求信息,自动帮我们创建Request结构体对象了。那么,net/http包能不能也自动帮我们创建Response结构体对象呢?当然不能。因为很显然,对于每个服务器程序,其行为是不同的,也即需要根据请求构建各样的响应信息,因此我们只能自己构建这个Response了。不过在这个过程中,net/http包还是竭尽所能地为我们提供帮助,从而帮我们隐去了许多复杂的信息。甚至如果不仔细想,我们都没有意识到我们是在构建Response结构体对象。
为了能更好地帮助我们,net/http包首先为我们规定了一个构建Response的标准过程。该过程就是要求我们实现一个Handler接口:
1
2
3
4 1type Handler interface {
2 ServeHTTP(ResponseWriter, *Request)
3}
4
现在,我们编写Web程序的主要工作就是编写各种实现该Handler接口的类型,并在该类型的ServeHTTP方法中编写服务器响应逻辑。这样一来,我们编写的Web服务器程序可能主要就是由各种各样的fooHandler、barHandler构成;Handler接口就成为net/http包中最重要的东西。可以说,每个Handler接口的实现就是一个小的Web服务器。以往由许多人将“handler”翻译为“句柄”,这里将其翻译为处理程序,或不做翻译。
该怎么实现此Handler接口呢?我们在这里提供多种方法。
方法1:显式地编写一个实现Handler接口的类型
我们已经读过Writing Web Applications这篇文章了,在其中曾实现了查看Wiki页面的功能。现在,让我们抛开其中的实现方法,以最普通的思维逻辑,来重现该功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 1package mainimport ( "fmt" "io/ioutil" "net/http") 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}
8
9type viewHandler struct{}
10
11func (viewHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
12 title := r.URL.Path[len("/view/"):]
13 p, _ := loadPage(title)
14 fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
15}func main() {
16 http.Handle("/view/", viewHandler{})
17 http.ListenAndServe(":8080", nil)
18}
19
假设该程序的当前目录中有一个abc.txt的文本文件,若访问http://localhost:8080/view/abc,则会显示该文件的内容。
在该程序main函数的第一行使用了Handle函数,其定义为:
1
2 1func Handle(pattern string, handler Handler)
2
该函数的功能就是将我们编写的Handler接口的实现viewHandler传递给net/http包,并由net/http包来调用viewHandler的ServeHTTP方法。至于如何生成Response,我们可以暂时不管,net/http包已经替我们完成这些工作了。
不过有一点还是要注意,该viewHandler只对URL的以/view/开头的路径才起作用,如果我们访问http://localhost:8080/或http://localhost:8080/edit,则都会返回一个404 page not found页面;而如果访问http://localhost:8080/view/xyz,则浏览器什么数据也得不到。对于后一种情况,很显然是因为我们编写的viewHandler.ServeHTTP方法没有对Wiki页面文件不存在时loadPage函数返回的错误进行处理造成的;而对前一种情况,则是net/http包帮我们完成的。很奇怪,为什么只是将/view/字符串传递给Handle函数的pattern参量,它就会比较智能地匹配viewHandler?而对于除了/view/开头路径的其他路径,由于没有显式地进行匹配,net/http包似乎也知道,并自动地帮我们返回404 page not found页面。这其实就是net/http包提供的简单的路由功能,我们将在以后对其进行介绍。
方法2:将一个普通函数转换为请求处理函数
我们可能已经注意到了,方法1中程序的viewHandler结构体中没有一个字段,我们构建它主要是为了使用其ServeHTTP方法。很显然,这有点绕了。因为在大多数时候,我们只需要使Handler成为一个函数就足够了。为此,http 包中提供了一个替代Handle函数的HandleFunc函数:
1
2 1func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
2
即HandleFunc函数不再像Handle那样接受一个Handler接口对象,而是接受一个具有特定签名的函数。而原来由Handler接口对象的ServeHTTP方法所实现的功能,现在需要该函数来实现。这样一来,我们就可以改写方法1中的示例程序了,这也正是Writing Web Applications一文所使用的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1package mainimport ( "fmt" "io/ioutil" "net/http") 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(w http.ResponseWriter, r *http.Request) {
8 title := r.URL.Path[len("/view/"):]
9 p, _ := loadPage(title)
10 fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
11}func main() {
12 http.HandleFunc("/view/", viewHandler)
13 http.ListenAndServe(":8080", nil)
14}
15
可以看出,该示例程序中的viewHandler函数实际上并没有实现Handler接口,因此它是一个伪Handler。不过其所实现的功能正是Handler接口对象需要实现的功能,我们可称像viewHandler这样的函数为Handler函数。我们会在方法3中通过类型转换轻易地将这种Handler函数转换为一个真正的Handler。
多数情况下,使用HandleFunc比使用Handle更加简便,这也是我们所常用的方法。
方法3:利用闭包功能编写一个返回Handler的请求处理函数
在Go语言中,函数是一等公民,函数字面可以被赋值给一个变量或直接调用。同时函数字面(实际上就是一段代码块)也是一个闭包,它可以引用定义它的外围函数(即该代码块的作用域环境)中的变量,这些变量会在外围函数和该函数字面之间共享,并且在该函数字面可访问期间一直存在。
那么,我们可以定义一个这样的函数类型,该函数类型具有和我们在方法2中定义的viewHandler函数具有相同的签名,因而可以通过类型转换把viewHandler函数转换为此函数类型;同时该函数类型本身实现了Handler接口。net/http包中的HandlerFunc就是这样的函数类型。
首先,HandlerFunc是一个函数类型:
1
2 1type HandlerFunc func(ResponseWriter, *Request)
2
其次,HandlerFunc同时也实现了Handler接口:
1
2 1func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
2
这里ServeHTTP的实现很简单,即调用其自身f(w, r)。
任何签名为func(http.ResponseWriter, *http.Request)函数都可以被转换为HandlerFunc。的事实上,方法2中的main函数中第一行的HandleFunc函数就是将viewHandler转换为HandlerFunc再针对其调用Handle的。即http.HandleFunc("/view/", viewHandler)相当于http.Handle("/view/", http.HandlerFunc(viewHandler{}))。
既然如此,能不能更直接地编写一个返回HandlerFunc函数的函数?借助于Go语言函数的灵活性,这一点是可以实现的。可对方法2中的viewHandler函数做如下改写:
1
2
3
4
5
6 1func viewHandler() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/view/"):]
2 p, _ := loadPage(title)
3 fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
4 })
5}
6
由于viewHandler函数返回的HandlerFunc对象既实现了Handler接口,又具有和方法2中的Handler函数相同的签名。因此此例中main函数的第一行既可以使用http.Handle,又可以使用http.HandleFunc。另外,该viewHandler函数中的return可以不用http.HandlerFunc进行显式类型转换,而是自动地将返回的函数字面转换为HandlerFunc类型。
现在理解起来可能变得困难点了。为什么要这样做呢?对比方法2和方法3的viewHandler函数签名就可以看出来了:方法2中的viewHandler函数签名必须是固定的,而方法3则是任意的。这样我们可以利用方法3向viewHandler函数中传递任意的东西,如数据库连接、HTML模板、请求验证、日志和追踪等东西,这些变量在闭包函数中是可访问的。而被传递的变量可以是定义在main函数内的局部变量;要不然,在闭包函数中能访问的外界变量就只能是全局变量了。另外,利用闭包的性质,被闭包函数引用的外部自由变量将与闭包函数一同存在,即在同样的引用环境中调用闭包函数时,其所引用的自由变量仍保持上次运行后的值,这样就达到了共享状态的目的。让我们对本例中的代码进行修改:
1
2
3
4
5
6
7
8
9
10
11
12
13 1func viewHandler(n int) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2 title := r.URL.Path[len("/view/"):]
3 p, _ := loadPage(title)
4 fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
5 n++
6 fmt.Fprintf(w, "<div>%v</div>", n)
7 })
8}func main() { var n int
9 http.HandleFunc("/view/", viewHandler(n))
10 http.HandleFunc("/page/", viewHandler(n))
11 http.ListenAndServe(":8080", nil)
12}
13
方法4:用封装器函数封装多个Handler的实现
我们就可以编写一个具有如下签名的HandlerFunc封装器函数:
1
2 1wrapperHandler(http.HandlerFunc) http.HandlerFunc
2
该封装器是这样一个函数,它具有一个输入参数和一个输出参数,两者都是HandlerFunc类型。该函数通常按如下方式进行定义:
1
2
3
4
5 1func wrapperHandler(f http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { do_something_before_calling_f()
2 f(w, r) do_something_after_calling_f()
3 })
4}
5
与方法3一样,在封装器函数中,我们使用了Go语言闭包的功能构建了一个函数变量,并在返回时将该函数变量转换为HandlerFunc。与方法3不一样的地方在于,我们通过一个参数将被封装的Handler函数传递给封装器函数,并在封装器函数中定义的闭包函数中通过通过f(w, r)调用被封装的HandlerFunc的功能。而在执行f(w, r)之前或之后,我们可以额外地做一些事情,甚至可以根据情况决定是否执行f(w, r)。
这样一来,可以在方法2的示例程序的基础上,添加wrapperHandler函数,并修改main函数:
1
2
3
4
5
6
7
8
9
10
11
12
13 1func wrapperHandler(f http.HandlerFunc) http.HandlerFunc {
2 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3 fmt.Fprintf(w, "<div>Do something <strong>before</strong> calling a handler.</div>")
4 f(w, r)
5 fmt.Fprintf(w, "<div>Do something <strong>after</strong> calling a handler.</div>")
6 })
7}
8
9func main() {
10 http.HandleFunc("/view/", wrapperHandler(viewHandler))
11 http.ListenAndServe(":8080", nil)
12}
13
我们真是绕了一个大圈,但这样绕有其自身的好处:
-
共享代码:将多个Handler函数(如viewHandler、editHandler和saveHandler)中共同的代码放进此封装器函数中,并在封装器中实现一些公用的代码,具体请见Writing Web Applications一文的末尾部分。
-
共享状态:除了本例中向wrapperHandler函数传递各种Handler函数外,我们可以增加参数个数,即传递其他自由变量给闭包(例如:func wrapperHandler(f http.HandlerFunc, n int) http.HandlerFunc),从而达到与方法3相同的共享状态效果。注意,这里说的共享状态实际上只是在同一个闭包函数(也即Handler)及其运行环境中共享状态,在某一运行环境下传递到某个闭包型Handler的自由变量并不能自动再被传出去,这与以后将要讲得在多个Handler间共享状态是不同的。
需要补充说明一下。在net/http包中,Handle和HandleFunc,Handler和HandlerFunc,都是对同一问题的具体两种方法。当我们处理的东西较简单时,为求简便,一般会用带Func后缀的后一类方法,尤其是HandlerFunc给我们带来了很大的灵活性。当需要定义一个包含较多字段的Handler实现时,就会像方法1那样正正经经地定义一个Handler类型。因此,不管是方法3和方法4,你都可以看到不同的写法,如使方法4封装的是Handler结构体变量而非这里的HandlerFunc,但其原理都是相通的。
ResponseWriter接口
尽管知道了Handler的多种写法,但我们还没有完全弄明白如何构建Response。net/http包将构建Response的过程也标准化了,即通过各种Handler操作ResponseWriter接口来构建Response。
1
2
3 1type ResponseWriter interface {
2 Header() Header Write([]byte) (int, error) WriteHeader(int) }
3
ResponseWriter实现了io.Writer接口,因此,该接口可被用于各种打印函数,如fmt.Fprintf。WriteHeader方法用于向HTTP响应信息写入状态码(一般是错误代码),它必须先于Write调用。若不调用WriteHeader,使用Write方法会自动写入状态码http.StatusOK。Header方法返回一个Header结构体对象,可以通过该结构体的方法对HTTP响应消息的头进行操作。但这种操作必须在WriteHeader和Write执行之前进行,除非所操作的Header字段在执行WriteHeader或Write之前已经被标记为"Trailer"。有点复杂,这里就不再多讲了。其实对于大部分人只要调用WriteHeader和Write就够了。