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