理解Go语言Web编程
aaanly
9年前
<p style="text-align:start">断断续续学Go语言很久了,一直没有涉及Web编程方面的东西。因为仅是凭兴趣去学习的,时间有限,每次去学,也只是弄个一知半解。不过这两天下定决心把Go语言Web编程弄懂,就查了大量资料,边学边记博客。希望我的这个学习笔记对其他人同样有帮助,由于只是业余半吊子学习,文中必然存在诸多不当之处,恳请读者留言指出,在此先道一声感谢!</p> <p style="text-align:start">本文只是从原理方面对Go的Web编程进行理解,尤其是详细地解析了<code>net/http</code>包。由于篇幅有限,假设读者已经熟悉<a href="/misc/goto?guid=4959543946573234098" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Writing Web Applications</a>这篇文章,这里所进行的工作只是对此文中只是的进一步深入学习和扩充。</p> <h3 style="text-align:start">Go语言Web程序的实质</h3> <p style="text-align:start">利用Go语言构建Web应用程序,实质上是构建<a href="/misc/goto?guid=4959670400963350919" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">HTTP</a>服务器。HTTP是一个简单的请求-响应协议,通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。下图为最简化的HTTP协议处理流程。</p> <p><img alt="理解Go语言Web编程" src="https://simg.open-open.com/show/2b60766b862e096e80a061cf4f3a70f9.png"><br> HTTP请求和响应流程</p> <p style="text-align:start">从上图可知,构建在服务器端运行的Web程序的基本要素包括:</p> <ul> <li>如何分析和表示HTTP请求;</li> <li>如何根据HTTP请求以及程序逻辑生成HTTP响应(包括生成HTML网页);</li> <li>如何使服务器端一直正确地运行以接受请求并生成响应。</li> </ul> <p style="text-align:start">Go语言有关Web程序的构建主要涉及<code>net/http</code>包,因此这里所给的各种函数、类型、变量等标识符,除了特别说明外,都是属于<code>net/http</code>包内的。</p> <h3 style="text-align:start">请求和响应信息的表示</h3> <p style="text-align:start">HTTP 1.1中,请求和响应信息都是由以下四个部分组成,两者之间格式的区别是开始行不同。</p> <ol> <li><strong>开始行</strong>。位于第一行。在请求信息中叫<strong>请求行</strong>,在响应信息中叫<strong>状态行</strong>。 <ul> <li>请求行:构成为<code>请求方法 URI 协议/版本</code>,例如<code>GET /images/logo.gif HTTP/1.1</code>;</li> <li>响应行:构成为<code>协议版本 状态代码 状态描述</code>,例如<code>HTTP/1.1 200 OK</code>。</li> </ul> </li> <li><strong>头</strong>。零行或多行。包含一些额外的信息,用来说明浏览器、服务器以及后续正文的一些信息。</li> <li>空行。</li> <li><strong>正文</strong>。包含客户端提交或服务器返回的一些信息。请求信息和响应信息中都可以没有此部分。</li> </ol> <p style="text-align:start">开始行和头的各行必须以<code><CR><LF></code>作为结尾。空行内必须只有<code><CR><LF></code>而无其他空格。在HTTP/1.1协议中,开始行和头都是以ASCII编码的纯文本,所有的请求头,除<code>Host</code>外,都是可选的。</p> <p style="text-align:start">HTTP请求信息由客户端发来,Web程序要做的首先就是分析这些请求信息,并用Go语言中响应的数据对象来表示。在<code>net/http</code>包中,用<code>Request</code>结构体表示HTTP请求信息。其定义为:</p> <pre style="text-align:start"> <code>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{} } </code></pre> <p style="text-align:start">当收到并<strong>理解</strong>(将请求信息解析为<code>Request</code>类型变量)了请求信息之后,就需要根据相应的处理逻辑,构建响应信息。<code>net/http</code>包中,用<code>Response</code>结构体表示响应信息。</p> <pre style="text-align:start"> <code>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 } </code></pre> <h3 style="text-align:start">如何构建响应信息</h3> <p style="text-align:start">很显然,前面给出的<code>Request</code>和<code>Response</code>结构体都相当复杂。好在客户端发来的请求信息是符合HTTP协议的,因此<code>net/http</code>包已经能够根据请求信息,自动帮我们创建<code>Request</code>结构体对象了。那么,<code>net/http</code>包能不能也自动帮我们创建<code>Response</code>结构体对象呢?当然不能。因为很显然,对于每个服务器程序,其行为是不同的,也即需要根据请求构建各样的响应信息,因此我们只能自己构建这个<code>Response</code>了。不过在这个过程中,<code>net/http</code>包还是竭尽所能地为我们提供帮助,从而帮我们隐去了许多复杂的信息。甚至如果不仔细想,我们都没有意识到我们是在构建<code>Response</code>结构体对象。</p> <p style="text-align:start">为了能更好地帮助我们,<code>net/http</code>包首先为我们规定了一个构建<code>Response</code>的标准过程。该过程就是要求我们实现一个<code>Handler</code>接口:</p> <pre style="text-align:start"> <code>type Handler interface { ServeHTTP(ResponseWriter, *Request) } </code></pre> <p style="text-align:start">现在,我们编写Web程序的主要工作就是编写各种实现该<code>Handler</code>接口的类型,并在该类型的<code>ServeHTTP</code>方法中编写服务器响应逻辑。这样一来,我们编写的Web服务器程序可能主要就是由各种各样的<code>fooHandler</code>、<code>barHandler</code>构成;<code>Handler</code>接口就成为<code>net/http</code>包中最重要的东西。可以说,每个<code>Handler</code>接口的实现就是一个小的Web服务器。以往由许多人将“handler”翻译为“句柄”,这里将其翻译为处理程序,或不做翻译。</p> <p style="text-align:start">该怎么实现此<code>Handler</code>接口呢?我们在这里提供多种方法。</p> <p>方法1:显式地编写一个实现<code>Handler</code>接口的类型</p> <p style="text-align:start">我们已经读过<a href="/misc/goto?guid=4959543946573234098" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Writing Web Applications</a>这篇文章了,在其中曾实现了查看Wiki页面的功能。现在,让我们抛开其中的实现方法,以最普通的思维逻辑,来重现该功能:</p> <pre style="text-align:start"> <code>package main import ( "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) } </code></pre> <p style="text-align:start">假设该程序的当前目录中有一个<code>abc.txt</code>的文本文件,若访问<code>http://localhost:8080/view/abc</code>,则会显示该文件的内容。</p> <p style="text-align:start">在该程序<code>main</code>函数的第一行使用了<code>Handle</code>函数,其定义为:</p> <pre style="text-align:start"> <code>func Handle(pattern string, handler Handler) </code></pre> <p style="text-align:start">该函数的功能就是将我们编写的<code>Handler</code>接口的实现<code>viewHandler</code>传递给<code>net/http</code>包,并由<code>net/http</code>包来调用<code>viewHandler</code>的<code>ServeHTTP</code>方法。至于如何生成<code>Response</code>,我们可以暂时不管,<code>net/http</code>包已经替我们完成这些工作了。</p> <p style="text-align:start">不过有一点还是要注意,该<code>viewHandler</code>只对<a href="/misc/goto?guid=4959670401064293234" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">URL</a>的以<code>/view/</code>开头的路径才起作用,如果我们访问<code>http://localhost:8080/</code>或<code>http://localhost:8080/edit</code>,则都会返回一个<code>404 page not found</code>页面;而如果访问<code>http://localhost:8080/view/xyz</code>,则浏览器什么数据也得不到。对于后一种情况,很显然是因为我们编写的<code>viewHandler.ServeHTTP</code>方法没有对Wiki页面文件不存在时<code>loadPage</code>函数返回的错误进行处理造成的;而对前一种情况,则是<code>net/http</code>包帮我们完成的。很奇怪,为什么只是将<code>/view/</code>字符串传递给<code>Handle</code>函数的<code>pattern</code>参量,它就会比较智能地匹配<code>viewHandler</code>?而对于除了<code>/view/</code>开头路径的其他路径,由于没有显式地进行匹配,<code>net/http</code>包似乎也知道,并自动地帮我们返回<code>404 page not found</code>页面。这其实就是<code>net/http</code>包提供的简单的路由功能,我们将在以后对其进行介绍。</p> <p>方法2:将一个普通函数转换为请求处理函数</p> <p style="text-align:start">我们可能已经注意到了,方法1中程序的<code>viewHandler</code>结构体中没有一个字段,我们构建它主要是为了使用其<code>ServeHTTP</code>方法。很显然,这有点绕了。因为在大多数时候,我们只需要使<code>Handler</code>成为一个函数就足够了。为此,<code>http</code> 包中提供了一个替代<code>Handle</code>函数的<code>HandleFunc</code>函数:</p> <pre style="text-align:start"> <code>func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) </code></pre> <p style="text-align:start">即<code>HandleFunc</code>函数不再像<code>Handle</code>那样接受一个<code>Handler</code>接口对象,而是接受一个具有特定签名的函数。而原来由<code>Handler</code>接口对象的<code>ServeHTTP</code>方法所实现的功能,现在需要该函数来实现。这样一来,我们就可以改写方法1中的示例程序了,这也正是<a href="/misc/goto?guid=4959543946573234098" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Writing Web Applications</a>一文所使用的方法:</p> <pre style="text-align:start"> <code>package main import ( "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) } </code></pre> <p style="text-align:start">可以看出,该示例程序中的<code>viewHandler</code>函数实际上并没有实现<code>Handler</code>接口,因此它是一个伪<code>Handler</code>。不过其所实现的功能正是<code>Handler</code>接口对象需要实现的功能,我们可称像<code>viewHandler</code>这样的函数为<code>Handler</code>函数。我们会在方法3中通过类型转换轻易地将这种<code>Handler</code>函数转换为一个真正的<code>Handler</code>。</p> <p style="text-align:start">多数情况下,使用<code>HandleFunc</code>比使用<code>Handle</code>更加简便,这也是我们所常用的方法。</p> <p>方法3:利用闭包功能编写一个返回<code>Handler</code>的请求处理函数</p> <p style="text-align:start">在Go语言中,函数是一等公民,函数字面可以被赋值给一个变量或直接调用。同时函数字面(实际上就是一段代码块)也是一个闭包,它可以引用定义它的外围函数(即该代码块的作用域环境)中的变量,这些变量会在外围函数和该函数字面之间共享,并且在该函数字面可访问期间一直存在。</p> <p style="text-align:start">那么,我们可以定义一个这样的函数类型,该函数类型具有和我们在方法2中定义的<code>viewHandler</code>函数具有相同的签名,因而可以通过类型转换把<code>viewHandler</code>函数转换为此函数类型;同时该函数类型本身实现了<code>Handler</code>接口。<code>net/http</code>包中的<code>HandlerFunc</code>就是这样的函数类型。</p> <p style="text-align:start">首先,<code>HandlerFunc</code>是一个函数类型:</p> <pre style="text-align:start"> <code>type HandlerFunc func(ResponseWriter, *Request) </code></pre> <p style="text-align:start">其次,<code>HandlerFunc</code>同时也实现了<code>Handler</code>接口:</p> <pre style="text-align:start"> <code>func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) </code></pre> <p style="text-align:start">这里<code>ServeHTTP</code>的实现很简单,即调用其自身<code>f(w, r)</code>。</p> <p style="text-align:start">任何签名为<code>func(http.ResponseWriter, *http.Request)</code>函数都可以被转换为<code>HandlerFunc</code>。的事实上,方法2中的<code>main</code>函数中第一行的<code>HandleFunc</code>函数就是将<code>viewHandler</code>转换为<code>HandlerFunc</code>再针对其调用<code>Handle</code>的。即<code>http.HandleFunc("/view/", viewHandler)</code>相当于<code>http.Handle("/view/", http.HandlerFunc(viewHandler{}))</code>。</p> <p style="text-align:start">既然如此,能不能更直接地编写一个返回<code>HandlerFunc</code>函数的函数?借助于Go语言函数的灵活性,这一点是可以实现的。可对方法2中的<code>viewHandler</code>函数做如下改写:</p> <pre style="text-align:start"> <code>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) }) } </code></pre> <p style="text-align:start">由于<code>viewHandler</code>函数返回的<code>HandlerFunc</code>对象既实现了<code>Handler</code>接口,又具有和方法2中的<code>Handler</code>函数相同的签名。因此此例中<code>main</code>函数的第一行既可以使用<code>http.Handle</code>,又可以使用<code>http.HandleFunc</code>。另外,该<code>viewHandler</code>函数中的<code>return</code>可以不用<code>http.HandlerFunc</code>进行显式类型转换,而是自动地将返回的函数字面转换为<code>HandlerFunc</code>类型。</p> <p style="text-align:start">现在理解起来可能变得困难点了。为什么要这样做呢?对比方法2和方法3的<code>viewHandler</code>函数签名就可以看出来了:方法2中的<code>viewHandler</code>函数签名必须是固定的,而方法3则是任意的。这样我们可以利用方法3向<code>viewHandler</code>函数中传递任意的东西,如数据库连接、HTML模板、请求验证、日志和追踪等东西,这些变量在闭包函数中是可访问的。而被传递的变量可以是定义在<code>main</code>函数内的局部变量;要不然,在闭包函数中能访问的外界变量就只能是全局变量了。另外,利用闭包的性质,被闭包函数引用的外部自由变量将与闭包函数一同存在,即在同样的引用环境中调用闭包函数时,其所引用的自由变量仍保持上次运行后的值,这样就达到了共享状态的目的。让我们对本例中的代码进行修改:</p> <pre style="text-align:start"> <code>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) } </code></pre> <p style="text-align:start">现在,分别访问<code>http://localhost:8080/view/abc</code>和<code>http://localhost:8080/page/abc</code>两个地址,每次刷新页面,则显示的<code>n</code>值增加1,但两个地址页面内的<code>n</code>值得变化是相互独立的。</p> <p>方法4:用封装器函数封装多个<code>Handler</code>的实现</p> <p style="text-align:start">我们就可以编写一个具有如下签名的<code>HandlerFunc</code>封装器函数:</p> <pre style="text-align:start"> <code>wrapperHandler(http.HandlerFunc) http.HandlerFunc </code></pre> <p style="text-align:start">该封装器是这样一个函数,它具有一个输入参数和一个输出参数,两者都是<code>HandlerFunc</code>类型。该函数通常按如下方式进行定义:</p> <pre style="text-align:start"> <code>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() }) } </code></pre> <p style="text-align:start">与方法3一样,在封装器函数中,我们使用了Go语言闭包的功能构建了一个函数变量,并在返回时将该函数变量转换为<code>HandlerFunc</code>。与方法3不一样的地方在于,我们通过一个参数将被封装的<code>Handler</code>函数传递给封装器函数,并在封装器函数中定义的闭包函数中通过通过<code>f(w, r)</code>调用被封装的<code>HandlerFunc</code>的功能。而在执行<code>f(w, r)</code>之前或之后,我们可以额外地做一些事情,甚至可以根据情况决定是否执行<code>f(w, r)</code>。</p> <p style="text-align:start">这样一来,可以在方法2的示例程序的基础上,添加<code>wrapperHandler</code>函数,并修改<code>main</code>函数:</p> <pre style="text-align:start"> <code>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) } </code></pre> <p style="text-align:start">我们真是绕了一个大圈,但这样绕有其自身的好处:</p> <ul> <li>共享代码:将多个<code>Handler</code>函数(如<code>viewHandler</code>、<code>editHandler</code>和<code>saveHandler</code>)中共同的代码放进此封装器函数中,并在封装器中实现一些公用的代码,具体请见<a href="/misc/goto?guid=4959543946573234098" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Writing Web Applications</a>一文的末尾部分。</li> <li>共享状态:除了本例中向<code>wrapperHandler</code>函数传递各种<code>Handler</code>函数外,我们可以增加参数个数,即传递其他自由变量给闭包(例如:<code>func wrapperHandler(f http.HandlerFunc, n int) http.HandlerFunc</code>),从而达到与方法3相同的共享状态效果。注意,这里说的共享状态实际上只是在同一个闭包函数(也即<code>Handler</code>)及其运行环境中共享状态,在某一运行环境下传递到某个闭包型<code>Handler</code>的自由变量并不能自动再被传出去,这与以后将要讲得在多个<code>Handler</code>间共享状态是不同的。</li> </ul> <p style="text-align:start">需要补充说明一下。在<code>net/http</code>包中,<code>Handle</code>和<code>HandleFunc</code>,<code>Handler</code>和<code>HandlerFunc</code>,都是对同一问题的具体两种方法。当我们处理的东西较简单时,为求简便,一般会用带<code>Func</code>后缀的后一类方法,尤其是<code>HandlerFunc</code>给我们带来了很大的灵活性。当需要定义一个包含较多字段的<code>Handler</code>实现时,就会像方法1那样正正经经地定义一个<code>Handler</code>类型。因此,不管是方法3和方法4,你都可以看到不同的写法,如使方法4封装的是<code>Handler</code>结构体变量而非这里的<code>HandlerFunc</code>,但其原理都是相通的。</p> <h3 style="text-align:start"><code>ResponseWriter</code>接口</h3> <p style="text-align:start">尽管知道了<code>Handler</code>的多种写法,但我们还没有完全弄明白如何构建<code>Response</code>。<code>net/http</code>包将构建<code>Response</code>的过程也标准化了,即通过各种<code>Handler</code>操作<code>ResponseWriter</code>接口来构建<code>Response</code>。</p> <pre style="text-align:start"> <code>type ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(int) } </code></pre> <p style="text-align:start"><code>ResponseWriter</code>实现了<code>io.Writer</code>接口,因此,该接口可被用于各种打印函数,如<code>fmt.Fprintf</code>。<code>WriteHeader</code>方法用于向HTTP响应信息写入状态码(一般是错误代码),它必须先于<code>Write</code>调用。若不调用<code>WriteHeader</code>,使用<code>Write</code>方法会自动写入状态码<code>http.StatusOK</code>。<code>Header</code>方法返回一个<code>Header</code>结构体对象,可以通过该结构体的方法对HTTP响应消息的头进行操作。但这种操作必须在<code>WriteHeader</code>和<code>Write</code>执行之前进行,除非所操作的<code>Header</code>字段在执行<code>WriteHeader</code>或<code>Write</code>之前已经被标记为<code>"Trailer"</code>。有点复杂,这里就不再多讲了。其实对于大部分人只要调用<code>WriteHeader</code>和<code>Write</code>就够了。</p> <h3 style="text-align:start"><code>ListenAndServe</code>函数</h3> <p style="text-align:start">前面所有示例程序中,都在<code>main</code>函数中调用了<code>ListenAndServe</code>函数。下面对此函数所做的工作进行分析。该函数的实现为:</p> <pre style="text-align:start"> <code>func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } </code></pre> <p style="text-align:start">该函数新建了一个<code>Server</code>对象,然后调用该<code>Server</code>的<code>ListenAndServe</code>方法并返回执行错误。</p> <p style="text-align:start"><code>Server</code>这个幕后大佬终于浮出水面了,基于<code>net/http</code>包建立的服务器程序都是它在操控的。让我们先看看该结构体的定义:</p> <pre style="text-align:start"> <code>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 } </code></pre> <p style="text-align:start">这里我们主要关心该结构体的<code>Addr</code>和<code>Handler</code>字段以及如下方法:</p> <pre style="text-align:start"> <code>func (srv *Server) ListenAndServe() error func (srv *Server) Serve(l net.Listener) error func (srv *Server) SetKeepAlivesEnabled(v bool) </code></pre> <p style="text-align:start"><code>ListenAndServe</code>在TCP网络地址<code>srv.Addr</code>上监听接入连接,并通过<code>Serve</code>方法处理连接。连接被接受后,则使TCP保持连接。如果<code>srv.Addr</code>为空,则默认使用<code>":http"</code>。<code>ListenAndServe</code>返回的<code>error</code>始终不为<code>nil</code>。</p> <p style="text-align:start"><code>Serve</code>在<code>net.Listener</code>类型的<code>l</code>上接受接入连接,为每个连接创建一个新的服务goroutine。该goroutine读请求并调用<code>srv.Handler</code>以进行响应。同<code>ListenAndServe</code>一样,<code>Serve</code>返回的<code>error</code>也一直不为<code>nil</code>。</p> <p style="text-align:start">至此我们已经涉及到了涉及更底层网络I/O的<code>net</code>包了,就不再继续深究了。</p> <p style="text-align:start">最简单的Web程序:</p> <pre style="text-align:start"> <code>package main import ( "net/http" ) func main() { http.ListenAndServe(":8080", nil) } </code></pre> <p style="text-align:start">这时访问<code>http://localhost:8080/</code>或其他任何路径并不是无法访问,而是得到前面提到的<code>404 page not found</code>。之所以能返回内容,正因为我们的服务器已经开始运行了,并且默认使用了<code>DefaultServeMux</code>这个<code>Handler</code>类型的变量。</p> <h3 style="text-align:start">路由</h3> <p><code>net/http</code>包默认的路由功能</p> <p style="text-align:start"><code>ServeMux</code>是<code>net/http</code>包自带的HTTP请求多路复用器(路由器)。其定义为:</p> <pre style="text-align:start"> <code>type ServeMux struct { mu sync.RWMutex m map[string]muxEntry hosts bool // whether any patterns contain hostnames } </code></pre> <p style="text-align:start"><code>ServeMux</code>的方法都是我们前面见过的函数或类型:</p> <pre style="text-align:start"> <code>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) </code></pre> <p style="text-align:start">每个<code>ServeMux</code>都包含一个映射列表,每个列表项主要将特定的URL模式与特定的<code>Handler</code>对应。为了方便,<code>net/http</code>包已经为我们定义了一个可导出的<code>ServeMux</code>类型的变量<code>DefaultServeMux</code>:</p> <pre style="text-align:start"> <code>var DefaultServeMux = NewServeMux() </code></pre> <p style="text-align:start">如果我们决定使用<code>ServeMux</code>进行路由,则在大部分情况下,使用<code>DefaultServeMux</code>已经够了。<code>net/http</code>包包括一些使用<code>DefaultServeMux</code>的捷径:</p> <ul> <li>调用<code>http.Handle</code>或<code>http.HandleFunc</code>实际上就是在往<code>DefaultServeMux</code>的映射列表中添加项目;</li> <li>若<code>ListenAndServe</code>的第二个参数为<code>nil</code>,它也默认使用<code>DefaultServeMux</code>。</li> </ul> <p style="text-align:start">当然,如果我们不嫌麻烦,可不用这个<code>DefaultServeMux</code>,而是自己定义一个。前面方法1中的<code>main</code>函数实现的功能与以下代码是相同的:</p> <pre style="text-align:start"> <code>func main() { mux := http.NewServeMux() mux.Handle("/view/", viewHandler{}) http.ListenAndServe(":8080", mux) } </code></pre> <p style="text-align:start">当我们往<code>ServeMux</code>对象中填充足够的列表项后,并在<code>ListenAndServe</code>函数中指定使用该路由器,则一旦HTTP请求进入,就会对该请求的一些部分(主要是<code>URL</code>)进行检查,找出最匹配的<code>Handler</code>对象以供调用,该对象可由<code>Handler</code>方法获得。如果<code>ServeMux</code>中已注册的任何URL模式都与接入的请求不匹配,<code>Handler</code>方法的第一个返回值也非<code>nil</code>,而是返回一个<code>NotFoundHandler</code>,其正文正是<code>404 page not found</code>,我们在前面已经见过它了。</p> <p style="text-align:start"><code>ServeMux</code>同时也实现了<code>Handler</code>接口。其<code>ServeHTTP</code>方法完成了<code>ServeMux</code>的主要功能,即根据HTTP请求找出最佳匹配的<code>Handler</code>并执行之,它本身就是一个多<code>Handler</code>封装器,是各个<code>Handler</code>执行的总入口。这使我们可以像使用其他<code>Handler</code>一样使用<code>ServeMux</code>对象,如将其传入<code>ListenAndServe</code>函数,真正地使我们的服务器按照<code>ServeMux</code>给定的规则运行起来。</p> <p>自定义路由实现</p> <p style="text-align:start"><code>ServeMux</code>的路由功能是非常简单的,其只支持路径匹配,且匹配能力不强。许多时候<code>Request.Method</code>字段是要重点检查的;有时我们还要检查<code>Request.Host</code>和<code>Request.Header</code>等字段。总之,在这些时候,<code>ServeMux</code>已经变得不够用了,这时我们可以自己编写一个路由器。由于前面讲的<code>Handle</code>或<code>HandleFunc</code>函数默认都使用<code>DefaultServeMux</code>,既然我们不再准备使用默认的路由器了,就不再使用这两个函数了。那么,只有向<code>ListenAndServe</code>函数传入我们的路由器了。根据<code>ListenAndServe</code>函数的签名,我们的路由器应首先是一个<code>Handler</code>,现在的问题变成该如何编写此<code>Handler</code>。很显然,此路由器<code>Handler</code>不仅自身是一个<code>Handler</code>,还需要能方便地将任务分配给其他<code>Handler</code>,为此,它必须有类似<code>Handle</code>或<code>HandleFunc</code>这样的函数,只不过这样的函数变得更强大、更通用,或更适合我们的业务。</p> <p style="text-align:start">我们已经知道<code>Handler</code>的实现有多种方法,现在我们需要考虑的是,我们的路由器应该是一个结构体还是一个函数。很显然,由于结构体具有额外的字段来存储其他信息,通常我们会希望我们的路由器是一个结构体,这样更利于功能的封装。以下程序实现了一个自定义的路由器<code>myRouter</code>,该路由器的功能就是对请求的域名(主机名称)进行检查,必须是已经注册的域名(可以有多个)才能访问网站功能。这样如果不借助像Nginx这样的反向代理,也可以限定我们的网站只为特定域名服务,而当其他不相关的域名也指向本服务器IP地址后,通过该域名访问此服务器将返回一个<code>404 site not found</code>页面。<code>myRouter.Add</code>方法的功能其实与<code>Handle</code>或<code>HandleFunc</code>类似。</p> <pre style="text-align:start"> <code>package main import ( "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) } </code></pre> <p>使用第三方路由包</p> <p style="text-align:start">以上自定义实现的<code>myRouter</code>实在是太简陋了,它主要适用于一些简单的Web服务器程序(如当下比较流行的单页面Web程序)。当网站程序较复杂时,我们就需要一个功能强大的路由器了。在<a href="/misc/goto?guid=4958183584851817768" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">GitHub</a>上已经有许多这样的路由器包了。如<a href="/misc/goto?guid=4959644326458258160" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">gorilla/mux</a>就是其中一例。该包的使用与<code>http.ServeMux</code>以及上面我们自己编写的<code>myRouter</code>基本相同,不过功能要强大好多。</p> <p style="text-align:start">另外还有一些路由实现包,其使用方法<code>http.ServeMux</code>稍有不同,如<a href="/misc/goto?guid=4959552653430024327" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">HttpRouter</a>。该包重新定义了<code>Handler</code>、<code>Handle</code>和<code>HandlerFunc</code>等类型或函数签名,因此要依照新的定义编写各种处理程序,所幸的是能有简单的方法继续使用原来的<code>http.Handler</code>和<code>http.HandlerFunc</code>。这里就不详细讲了。</p> <h3 style="text-align:start">中间件</h3> <p>什么是中间件</p> <p style="text-align:start">在前面路由器的实现中,我们已经意识到,通常只有尽量使用各种现成的包提供的功能,才能使我们编写Web服务器程序更加轻松。为了方便我们使用,这些现成的包通常以中间件的形式提供。所谓中间件,是指程序的一部分,它可以封装已有的程序功能,并且添加额外的功能。对于Go语言的Web编程来说,中间件就是在HTTP请求-响应处理链上的函数,他们是独立于我们的Web程序而编写,并能够访问我们的请求、响应以及其他需要共享的变量。在<a href="/misc/goto?guid=4958183584851817768" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">GitHub</a>能找到许多Go语言写的HTTP中间件,这些中间件都以独立的包提供,这意味着他们是独立的,可以方便地添加到程序,或从中移除。</p> <p style="text-align:start">在上面的方法4中,我们在不经意间写出了一个中间件。这里的<code>wrapperHandler</code>就是一个中间件,它就像一个喇叭外面的盒子,不仅将喇叭包起来成为一个音箱,还为音箱添加了电源开关、调节音量大小等功能。只要这个盒子的大小合适,它还可以用来包装其他的喇叭而构成不同的音箱。进一步地,我们甚至可以认为各种路由器(如我们前面写的<code>myRouter</code>)其实也是中间件。</p> <p style="text-align:start">Go语言的中间件实现的要点:</p> <ul> <li>中间件自身是一个<code>Handler</code>类型;或者是一个返回<code>Handler</code>类型的函数;或是一个返回<code>HandlerFunc</code>的函数;或者是返回一个函数,该函数的返回值为<code>Handler</code>类型(真够绕的)。</li> <li>中间件一般封装一个(或多个)<code>Handler</code>,并在适当的位置调用该<code>Handler</code>,如通过调用<code>f(w, r)</code>将<code>w http.ResponseWriter, r *http.Request</code>两参数传递给被封装的<code>Handler</code>并执行之。</li> <li>在调用<code>Handler</code>之前或之后,可以实现自身的一些功能。</li> <li>通过一定的机制在多个<code>Handler</code>之间共享状态。</li> </ul> <p style="text-align:start"><a href="/misc/goto?guid=4959670401253887997" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">gorilla/handlers</a>包就提供了许多的中间件,他们的定义与上面的<code>wrapperHandler</code>不太相同,让我们来随便看看其中一些中间件的函数签名:</p> <pre style="text-align:start"> <code>func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler func CompressHandler(h http.Handler) http.Handler </code></pre> <p style="text-align:start">通常中间件实现的功能都是大多数Web服务器程序共同需要的功能。如:</p> <ul> <li>日志记录和追踪,显示调试信息;</li> <li>连接或断开数据库连接;</li> <li>提供静态文件HTTP服务;</li> <li>验证请求信息,阻止恶意的或其他不想要的访问,限制访问频次;</li> <li>写响应头,压缩HTTP响应,添加<a href="/misc/goto?guid=4958870899875925229" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">HSTS</a>头;</li> <li>从异常中恢复运行;</li> <li>等等……</li> </ul> <p>组合使用各种中间件</p> <p style="text-align:start">理解了中间件的概念以及其使用和编写方法之后,编写我们自己的Web服务器程序就不那么复杂了:无非就是编写各种各样的<code>Handler</code>,并仔细设计将这些<code>Handler</code>层层组合起来。当然这其中必然会涉及更多的知识,但那些都是细节了,我们这里并不进行讨论。</p> <p style="text-align:start">进一步的学习或应用可以结合已有的一些第三方中间件库来编写自己的程序,如<a href="/misc/goto?guid=4958544836657147632" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Gorilla</a> Web工具箱或<a href="/misc/goto?guid=4958838691662575347" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">codegangsta/negroni</a>。这两者的共同特点就是遵照<code>net/http</code>包的惯用法进行编程,只要理解了前面讲的知识,就能较轻易地理解这两者的原理和用法。这两者之中,<a href="/misc/goto?guid=4958838691662575347" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">codegangsta/negroni</a>的聚合度要更高一点,它主动帮我们实现了一些常用功能。</p> <p style="text-align:start">当有人在社区中问究竟该使用哪个Go语言Web框架时,总会有人回答说使用<code>net/http</code>包自身的功能就是不错的选择,这种回答实际上就是自己按照以上讲述的方法编写各种具体功能的<code>Handler</code>,并使用网上已有的各种中间件,从而实现程序功能。现在看来,由于<code>net/http</code>包以及Go语言的出色设计,这样的确能编写出灵活的且具有较大扩展性的程序,这种方法的确是一种不错的选择。但尽管如此,有时我们还是希望能有别人帮我们做更多的事情,甚至已经为我们规划好了程序的结构,这个时候,我们就要使用到框架。</p> <h3 style="text-align:start">在多个<code>Handler</code>(或中间件)间共享状态</h3> <p style="text-align:start">当我们的Web服务器程序的体量越来越大时,就必然有许许多多的<code>Handler</code>(中间件也是<code>Handler</code>);对于同一个请求,可能需要多个<code>Handler</code>进行处理;多个<code>Handler</code>被并列地或嵌套地调用。因此,这时就会涉及到多个<code>Handler</code>之间共享状态(即共享变量)的问题。在前面我们已经见识过中间件的编写方式,就是提供各种方法将<code>w http.ResponseWriter</code>和<code>r *http.Request</code>参数先传递给中间件(封装器),然后再进一步传递给被封装的<code>Handler</code>或<code>HandlerFunc</code>,这里传递的<code>w</code>和<code>r</code>变量实际上就是被共享的状态。</p> <p style="text-align:start">通常,有两类变量需要在多个<code>Handler</code>间共享。第一类是在服务器运行期间一直存在,且被多个<code>Handler</code>共同使用的变量,如一个数据库连接,存储session所用的仓库,甚至前面讲的<code>ServeMux</code>中存储<code>pattern</code>和<code>Handler</code>间对应关系的列表等,我们将第一类变量称作“与应用程序同生存周期的变量”。第二类是只在单个请求的处理期间存在的变量,如从<code>Request</code>信息中得出的用户ID和授权码等,我们将第二类变量称作“与请求同生存周期变量”,对于不同的请求,需要的这种变量的类型、个数都不固定。</p> <p style="text-align:start">另外,在Go语言中,每次请求处理都需要启动一个独立的goroutine,这时在<code>Handler</code>间共享状态还不涉及线程安全问题;但有些请求的处理过程中可能会启动更多的goroutine,如某个处理请求的goroutine中,再启动一个goroutine进行RPC,这时在多个<code>Handler</code>间共享状态时,要确保该变量是线程安全的,即不能在某个goroutine修改某个变量的同时,另外一个goroutine在读此变量。如果将同一个变量传递给多个goroutine,一旦该变量被修改或设为不可用,这种改变对所有goroutine应该是一致的。当编写Web程序时,常常遇到与请求同生存周期变量,我们往往无法精确预料需要保存的变量类型和变量个数,这时最方便的是使用映射类型进行保存,而映射又不是线程安全的。因此,必须采取措施保证被传递的变量是线程安全的。</p> <p style="text-align:start">在多个<code>Handler</code>间传递变量的方法可归结为两种:</p> <p>方法a:使用全局变量共享状态</p> <p style="text-align:start">如在包的开头定义一个全局变量</p> <pre style="text-align:start"> <code>var db *sql.DB </code></pre> <p style="text-align:start">前面讲到的在<code>http</code>包中定义的<code>http.DefaultServeMux</code>就是这样的全局变量。</p> <p style="text-align:start">这样我们自己编写的各个<code>Handler</code>就可以直接访问此全局变量了。对于第一类的与应用程序同生存周期的变量,这是一个好办法。但当我们的程序中有太多的<code>Handler</code>时,每个<code>Handler</code>可能都需要一些特别的全局变量,这时程序中可能有很多的全局变量,就会增加程序的耦合度,使维护变得困难。这时可以用结构体类型进一步封装这些全局变量,甚至把<code>Handler</code>定义为这种结构体的方法。</p> <p style="text-align:start">对于与请求同生存周期变量,也可以使用全局变量的方法在多个<code>Handler</code>之间共享状态。<a href="/misc/goto?guid=4959548051266168951" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">gorilla/context</a>包就提供了这样一种功能。该包提供一种方法在一个全局变量中存储很多很多的东西,且可以线程安全地读写。该包中的一个全局变量可用来存储在一个请求生命周期内需要共享的东西。每次的请求是不同的,每次请求所要共享的状态也是不同的,为了实现最大限度的灵活性,该包差不多定义了一个具有以下类型的全局变量:</p> <pre style="text-align:start"> <code>map[*http.Request]map[string]interface{} </code></pre> <p style="text-align:start">该全局变量针对每次请求存储一组状态的列表,在请求结束将该请求对应的状态映射列表清空。由于是用映射实现的,而映射并非线程安全的,因此在每次数据项改写操作过程中需要将其锁起来。</p> <p>方法b:修改<code>Handler</code>的定义通过传递参数共享状态</p> <p style="text-align:start">既然<code>w http.ResponseWriter</code>和<code>r *http.Request</code>就是在各个<code>Handler</code>之间共享的两个状态变量,那能不能修改<code>http</code>包,以同样的方法共享更多的状态变量?当然能,并且还有多种方法:</p> <p style="text-align:start">示例1:修改<code>Handler</code>接口的<code>ServeHTTP</code>函数签名,使其接受一个额外的参数。如使其变为<code>ServeHTTP(http.ResponseWriter, *http.Request, int)</code>,从而可额外将一个<code>int</code>类型变量(如用户ID)传递给<code>Handler</code>。</p> <p style="text-align:start">示例2:修改<code>Request</code>,使其包含需要共享的额外的字段。</p> <p style="text-align:start">示例3:设计一个类型,使它既包含<code>Request</code>的内容,又实现了<code>ResponseWriter</code>接口,同时又可包含额外的变量。</p> <p style="text-align:start">还有更多种方法,既然不再必须遵守<code>http</code>包中关于<code>Handler</code>实现的约定,我们可以随心所欲地编写我们的<code>Handler</code>。这种方法对于与请求同生存周期变量的共享非常有用。已经存在着许许多多的Go语言Web框架,往往每种框架都规定了一种编写<code>Handler</code>的方法,都能更方便地在各个<code>Handler</code>之间共享状态。我们似乎获得了更大的自由,但请注意,这样一来,我们往往需要修改<code>http</code>包中的许多东西,并且不使用惯用的方法来编写<code>Handler</code>或中间件,使得各个<code>Handler</code>或中间件对不同的框架是不通用的。因此,这些为了更好地实现在多个<code>Handler</code>间共享状态的方法,反倒使Go语言的Web编程世界变得支离破碎。</p> <p style="text-align:start">还需要说明一点。我们提倡编写标准的<code>Handler</code>来使我们的代码更容易调用第三方中间件或被第三方中间件调用,但并不意味着在编程时,所有的处理函数或类型都要编写成<code>Handler</code>形式,因为这样反而会限制了我们的自由。只要我们的函数或类型不是可导出的,并且不与其他中间件交互,我们就可以随意地编写他们。这样一来,函数或方法就可以随意地定义,共享状态并不是那么难。</p> <p>通过上下文(context)共享状态</p> <p style="text-align:start">Context通常被译作上下文或语境,它是一个比较抽象的概念,可以将其理解为程序单元的一个运行状态(或快照)。这里的程序单元可以为一个goroutine,或为一个<code>Handler</code>。如每个goroutine在执行之前,都要先知道整个程序当前的执行状态,通常将这些执行状态封装在一个<code>ctx</code>(context的缩写)结构体变量中,传递给要执行的goroutine中。上下文的概念几乎已经成为传递与请求同生存周期变量的标准方法,这时<code>ctx</code>不光要在多个<code>Handler</code>之间传递,同时也可能在多个goroutine之间传递,因此我们必须保证所传递的<code>ctx</code>变量是类型安全的。</p> <p style="text-align:start">所幸的是,已经存在一种成熟的机制在多个goroutine间线程安全地传递变量了,具体请参见<a href="/misc/goto?guid=4958977041302305475" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Go Concurrency Patterns: Context</a>,<a href="/misc/goto?guid=4959670401499447735" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">golang.org/x/net/context</a>包就是这种机制的实现。<code>context</code>包不仅实现了在程序单元(goroutine、API边界等)之间共享状态变量的方法,同时能通过简单的方法,使我们在被调用程序单元的外部,通过设置<code>ctx</code>变量值,将过期或撤销这些信号传递给被调用的程序单元。</p> <p style="text-align:start">在Go 1.7中,<code>context</code>可能作为最顶层的包进入标准库。<code>context</code>包能被应用于多种场合,但最主要的场合应该是在多个goroutine间(其实也是在多个<code>Handler</code>间)方便、安全地共享状态。为此,在Go 1.7中,随着<code>context</code>包的引入,将会在<code>http.Request</code>结构体中添加一个新的字段<code>Context</code>。这种方法正是前面方法b中的示例2所做的,这样一来,我们就定义了一种在多个<code>Handler</code>间共享状态的标准方法,有可能使Go语言已经开始变得破碎的Web编程世界得以弥合。</p> <p style="text-align:start">既然<code>context</code>包这么重要,让我们来了解一下它吧。<code>context</code>包的核心就是<code>Context</code>接口,其定义如下:</p> <pre style="text-align:start"> <code>type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} } </code></pre> <p style="text-align:start">该接口的<code>Value</code>方法返回与一个<code>key</code>(不存在<code>key</code>时就用<code>nil</code>)对应的值,该值就是<code>ctx</code>要传递的具体变量值。除此之外,我们定义了专门的方法来额外地标明某个<code>Context</code>是否已关闭(超过截止时间或被主动撤销)、关闭的时间及原因:<code>Done</code>方法返回一个信道(channel),当<code>Context</code>被撤销或过期时,该信道是关闭的,即它是一个表示<code>Context</code>是否已关闭的信号;当<code>Done</code>信道关闭后,<code>Err</code>方法表明<code>Context</code>被撤的原因;当<code>Context</code>将要被撤销时,<code>Deadline</code>返回撤销执行的时间。在Web编程时,<code>Context</code>对象总是与一个请求对应的,若<code>Context</code>已关闭,则与该请求相关联的所有goroutine应立即释放资源并退出。</p> <p style="text-align:start">似乎<code>Context</code>接口没有提供方法来设置其值和过期时间,也没有提供方法直接将其自身撤销。也就是说,<code>Context</code>不能改变和撤销其自身。那么该怎么通过<code>Context</code>传递改变后的状态呢?请继续读下去吧。</p> <p style="text-align:start">无论是goroutine,他们的创建和调用关系总是像一棵树的根系一样层层进行的,更靠根部的goroutine应有办法主动关闭其下属的goroutine的执行(不然程序可能就失控了)。为了实现这种关系,我们的<code>Context</code>结构也应该像一棵树的根系,根须总是由根部衍生出来的。要创建<code>Context</code>树,第一步就是要得到树根,<code>context.Background</code>函数的返回值就是树根:</p> <pre style="text-align:start"> <code>func Background() Context </code></pre> <p style="text-align:start">该函数返回一个非nil但值为空的<code>Context</code>,该<code>Context</code>一般由<code>main</code>函数创建,是与进入请求对应的<code>Context</code>树的树根,它不能被取消、没有值、也没有过期时间。</p> <p style="text-align:start">有了树根,又该怎么创建根须呢?<code>context</code>包为我们提供了多个函数来创建根须:</p> <pre style="text-align:start"> <code>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 </code></pre> <p style="text-align:start">看见没有?这些函数都接收一个<code>Context</code>类型的参数<code>parent</code>,并返回一个<code>Context</code>类型的值,即表示是从接收的根部得到返回的须部。这些函数都是树形结构上创建根部的须部,须部是从复制根部得到的,并且根据接收参数设定须部的一些状态值,接着就可以将根须传递给下层的goroutine了。</p> <p style="text-align:start">让我们先来看看最后面的<code>WithValue</code>函数,它返回<code>parent</code>的一个副本,调用该副本的<code>Value(key)</code>方法将得到<code>val</code>。这样我们不光将根部原有的值保留了,还在须部中加入了新的值(若须部新加入值的<code>key</code>在根部已存在,则会覆盖根部的值)。</p> <p style="text-align:start">我们还不知道该怎么设置<code>Context</code>的过期时间,或直接撤销<code>Context</code>呢,答案就在前三个函数。先看第一个<code>WithCancel</code>函数,它只是将根部复制到须部,并且还返回一个额外的<code>cancel CancelFunc</code>函数类型变量,该函数类型的定义为:</p> <pre style="text-align:start"> <code>type CancelFunc func() </code></pre> <p style="text-align:start">调用<code>CancelFunc</code>对象将撤销对应的<code>Context</code>对象,这就是主动撤销<code>Context</code>的方法。也就是说,在根部<code>Context</code>所对应的环境中,通过<code>WithCancel</code>函数不仅可创建须部的<code>Context</code>,同时也获得了该须部<code>Context</code>的一个命门机关,只要一触发该机关,该须部<code>Context</code>(以及须部的须部)都将一命呜呼。</p> <p style="text-align:start"><code>WithDeadline</code>函数的作用也差不多,它返回的<code>Context</code>类型值同样是<code>parent</code>的副本,但其过期时间由<code>deadline</code>和<code>parent</code>的过期时间共同决定。当<code>parent</code>的过期时间早于传入的<code>deadline</code>时间时,返回的根须过期时间应与<code>parent</code>相同(根部过期时,其所有的根须必须同时关闭);反之,返回的根须的过期时间则为<code>deadline</code>。<code>WithTimeout</code>函数又和<code>WithDeadline</code>类似,只不过它传入的是从现在开始<code>Context</code>剩余的生命时长。<code>WithDeadline</code>和<code>WithTimeout</code>同样也都返回了所创建的子<code>Context</code>的命门机关:一个<code>CancelFunc</code>类型的函数变量。</p> <p style="text-align:start"><code>context</code>包实现的功能使得根部<code>Context</code>所处的环境总是对须部<code>Context</code>有生杀予夺的大权。这样一来,我们的根部goroutine对须部的goroutine也就有了控制权。</p> <p style="text-align:start">概括来说,在请求处理时,上下文具有如下特点:</p> <ul> <li><code>Context</code>对象(<code>ctx</code>变量)的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个<code>ctx</code>变量(它为<code>Context</code>树结构的树根);在请求处理结束后,撤销此<code>ctx</code>变量,释放资源。</li> <li>每次创建一个goroutine或调用一个<code>Handler</code>,要么将原有的<code>ctx</code>传递给goroutine,要么创建<code>ctx</code>的一个子<code>Context</code>并传递给goroutine。</li> <li>为了使多个中间件相互链式调用,必须以标准的方法在多个<code>Handler</code>之间传递<code>ctx</code>变量。如重新规定<code>Handler</code>接口中<code>ServeHTTP</code>方法的签名为<code>ServeHTTP(context.Context, http.ResponseWriter, *http.Request)</code>,或将<code>Context</code>作为<code>Request</code>结构体的一个字段。</li> <li><code>ctx</code>对象能灵活地存储不同类型、不同数目的值,并且使多个goroutine安全地读写其中的值。</li> <li>当通过父<code>Context</code>对象创建子<code>Context</code>对象时,可同时获得子<code>Context</code>的一个撤销函数,这样父<code>Context</code>对象的创建环境就获得了对子<code>Context</code>将要被传递到的goroutine的撤销权。</li> <li>在子<code>Context</code>被传递到的goroutine中,应该对该子<code>Context</code>的<code>Done</code>信道(channel)进行监控,一旦该信道被关闭(即上层运行环境撤销了本goroutine的执行),应主动终止对当前请求信息的处理,释放资源并返回。</li> </ul> <p style="text-align:start">现在,是时候给出点示例代码来看看<code>context</code>包具体该如何应用了。但由于篇幅所限,加之短短几行代码难以说明白<code>context</code>包的用法,这里并不准备进行举例。<a href="/misc/goto?guid=4958977041302305475" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Go Concurrency Patterns: Context</a>一文中所列举的“Google Web Search”示例则是一个极好的学习示例,请自行移步去看吧。</p> <h3 style="text-align:start">框架</h3> <p style="text-align:start">我们在前面已经费劲口舌地说明了当用Go写Web服务器程序时,该如何实现路由功能,以及该如何用规范的方式编写<code>Handler</code>(或中间件)。但一个Web程序的编写往往要涉及更多的方面,我们在前面介绍中间件时已经说过,各种各样的中间件能够帮助我们完成这些任务。但许多时候,我们总是希望他人帮我们完成更多的事情,从而使我们自己的工作更加省力。应运这种需求,就产生了许许多多的Web框架。根据架构的不同,这些框架大致可分为两大类:</p> <p style="text-align:start">第一类是微架构型框架。其核心框架只提供很少的功能,而更多的功能则需要组合各种中间件来提供,因此这种框架也可称为混搭型框架。它相当灵活,但相对来说需要使用者在组合使用各种中间件时花费更大的力气。像<a href="/misc/goto?guid=4959643552046485273" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Echo</a>、<a href="/misc/goto?guid=4959649798960952878" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Goji</a>、<a href="/misc/goto?guid=4959615895338080497" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Gin</a>等都属于微架构型框架。</p> <p style="text-align:start">第二类是全能型架构。它基本上提供了你编写Web应用时需要的所有功能,因此更加重型,多数使用MVC架构模式设计。在使用这类框架时你可能感觉更轻省,但其做事风格一般不同于Go语言惯用的风格,你也较难弄明白这些框架是如何工作的。像<a href="/misc/goto?guid=4958861952928870474" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Beego</a>、<a href="/misc/goto?guid=4958855626447520330" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Revel</a>等就属于全能型架构。</p> <p style="text-align:start">对于究竟该选择微架构还是全能型架构,仍有较多的争议。像<a href="/misc/goto?guid=4959670401728796787" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">The Case for Go Web Frameworks</a>一文就力挺全能型架构,并且其副标题就是“Idiomatic Go is not a religion”,但该文也收到了较多的反对意见,见<a href="/misc/goto?guid=4959670401808734165" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">这里</a>和<a href="/misc/goto?guid=4959670401888599263" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">这里</a>。总体上来说,Go语言社区已越来越偏向使用微架构型框架,当将来<code>context</code>包进入标准库后,<code>http.Handler</code>本身就定义了较完善的中间件编写规范,这种使用微架构的趋势可能更加明显,并且各种微架构的实现方式有望进一步走向统一,这样其实<code>http</code>包就是一个具有庞大生态系统的微架构框架。</p> <h3 style="text-align:start">更加自我</h3> <p style="text-align:start">在此之前,我们一直在谈论<code>net/http</code>包,但实际上我们甚至可以完全不用此包而编写Web服务器程序。如有人编写了<a href="/misc/goto?guid=4959649787265840401" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">fasthttp</a>包,并声称它比<code>net/http</code>包快10倍,并且前面提到的<a href="/misc/goto?guid=4959643552046485273" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Echo</a>框架也可以在底层使用此包。听起来或许很好,但这样一来,我们编写<code>Handler</code>和中间件的方式就会大变了,最终可能置我们于孤独的境地。</p> <p style="text-align:start">这里之所以介绍<a href="/misc/goto?guid=4959649787265840401" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">fasthttp</a>包,只是为了告诉大家,我们总有更多的选择,千万不要把思维局限在某种方法或某个框架。随着我们对自身需求把握得更加准确,以及对程序质量要求的提高,我们可能真的会去考虑这些选择,而到那时,则必须对Go语言Web编程有更深刻的理解。</p> <h3 style="text-align:start">参考文章</h3> <ul> <li><a href="/misc/goto?guid=4959670402033040354" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Writing HTTP Middleware in Go</a></li> <li><a href="/misc/goto?guid=4959670402116361688" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">The http.HandlerFunc wrapper technique in #golang</a></li> <li><a href="/misc/goto?guid=4959670402201395908" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Building Web Apps with Go</a></li> <li><a href="/misc/goto?guid=4959670402288916223" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Build Web Application with Golang</a></li> <li><a href="/misc/goto?guid=4959670402375690739" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Custom variables in http.Request</a></li> <li><a href="/misc/goto?guid=4958977041302305475" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Go Concurrency Patterns: Context</a></li> <li><a href="/misc/goto?guid=4959670402473919636" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Go’s net/context and http.Handler</a></li> <li><a href="/misc/goto?guid=4959670402565246300" style="box-sizing: border-box; color: rgb(1, 145, 200); text-decoration: none; background: transparent;">Context of incoming request</a></li> </ul> <p>来源:http://www.chingli.com/coding/understanding-go-web-app/</p>