Go Web 编程之 程序结构
概述
一个典型的 Go Web 程序结构如下,摘自《Go Web 编程》:

- 客户端发送请求;
- 服务器中的多路复用器收到请求;
- 多路复用器根据请求的 URL 找到注册的处理器,将请求交由处理器处理;
- 处理器执行程序逻辑,必要时与数据库进行交互,得到处理结果;
- 处理器调用模板引擎将指定的模板和上一步得到的结果渲染成客户端可识别的数据格式(通常是 HTML);
- 最后将数据通过响应返回给客户端;
- 客户端拿到数据,执行对应的操作,如渲染出来呈现给用户。
本文介绍如何创建多路复用器,如何注册处理器,最后再简单介绍一下 URL 匹配。我们以上一篇文章中的"Hello World"程序作为基础。
package main
import (
"fmt"
"log"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}
func main() {
http.HandleFunc("/", hello)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
多路复用器
默认多路复用器
net/http 包为了方便我们使用,内置了一个默认的多路复用器DefaultServeMux。定义如下:
// src/net/http/server.go
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
这里给大家介绍一下 Go 标准库代码的组织方式,便于大家对照。
- Windows上,Go 语言的默认安装目录为
C:\Go,即GOROOT; GOROOT下有一个 src 目录,标库库的代码都在这个目录中;- 每个包有一个单独的目录,例如 fmt 包在
src/fmt目录中; - 子包在其父包的子目录中,例如 net/http 包在
src/net/http目录中。
net/http 包中很多方法都在内部调用DefaultServeMux的对应方法,如HandleFunc。我们知道,HandleFunc是为指定的 URL 注册一个处理器(准确来说,hello是处理器函数,见下文)。其内部实现如下:
// src/net/http/server.go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
实际上,http.HandleFunc方法是将处理器注册到DefaultServeMux中的。
另外,我们使用 ":8080" 和 nil 作为参数调用http.ListenAndServe时,会创建一个默认的服务器:
// src/net/http/server.go
func ListenAndServe(addr string, handler Handler) {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
这个服务器默认使用DefaultServeMux来处理器请求:
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
handler.ServeHTTP(rw, req)
}
服务器收到的每个请求会调用对应多路复用器(即ServeMux)的ServeHTTP方法。在ServeMux的ServeHTTP方法中,根据 URL 查找我们注册的处理器,然后将请求交由它处理。
虽然默认的多路复用器使用起来很方便,但是在生产环境中不建议使用。由于DefaultServeMux是一个全局变量,所有代码,包括第三方代码都可以修改它。
有些第三方代码会在DefaultServeMux注册一些处理器,这可能与我们注册的处理器冲突。
比较推荐的做法是自己创建多路复用器。
创建多路复用器
创建多路复用器也比较简单,直接调用http.NewServeMux方法即可。然后,在新创建的多路复用器上注册处理器:
package main
import (
"fmt"
"log"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", hello)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
上面代码的功能与 "Hello World" 程序相同。这里我们还自己创建了服务器对象。通过指定服务器的参数,我们可以创建定制化的服务器。
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
}
在上面代码,我们创建了一个读超时和写超时均为 1s 的服务器。
处理器和处理器函数
上文中提到,服务器收到请求后,会根据其 URL 将请求交给相应的处理器处理。处理器是实现了Handler接口的结构,Handler接口定义在 net/http 包中:
// src/net/http/server.go
type Handler interface {
func ServeHTTP(w Response.Writer, r *Request)
}
我们可以定义一个实现该接口的结构,注册这个结构类型的对象到多路复用器中:
package main
import (
"fmt"
"log"
"net/http"
)
type GreetingHandler struct {
Language string
}
func (h GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", h.Language)
}
func main() {
mux := http.NewServeMux()
mux.Handle("/chinese", GreetingHandler{Language: "你好"})
mux.Handle("/english", GreetingHandler{Language: "Hello"})
server := &http.Server {
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
与前面的代码有所不同,上段代码中,定义了一个实现Handler接口的结构GreetingHandler。然后,创建该结构的两个对象,分别将它注册到多路复用器的/hello和/world路径上。注意,这里注册使用的是Handle方法,注意与HandleFunc方法对比。
启动服务器之后,在浏览器的地址栏中输入localhost:8080/chinese,浏览器中将显示你好,输入localhost:8080/english将显示Hello。
虽然,自定义处理器这种方式比较灵活,强大,但是需要定义一个新的结构,实现ServeHTTP方法,还是比较繁琐的。为了方便使用,net/http 包提供了以函数的方式注册处理器,即使用HandleFunc注册。函数必须满足签名:func (w http.ResponseWriter, r *http.Request)。
我们称这个函数为处理器函数。我们的 "Hello World" 程序中使用的就是这种方式。HandleFunc方法内部,会将传入的处理器函数转换为HandlerFunc类型。
// src/net/http/server.go
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
HandlerFunc是底层类型为func (w ResponseWriter, r *Request)的新类型,它可以自定义其方法。由于HandlerFunc类型实现了Handler接口,所以它也是一个处理器类型,最终使用Handle注册。
// src/net/http/server.go
type HandlerFunc func(w *ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
注意,这几个接口和方法名很容易混淆,这里再强调一下:
Handler:处理器接口,定义在 net/http 包中。实现该接口的类型,其对象可以注册到多路复用器中;Handle:注册处理器的方法;HandleFunc:注册处理器函数的方法;HandlerFunc:底层类型为func (w ResponseWriter, r *Request)的新类型,实现了Handler接口。它连接了处理器函数与处理器。
URL 匹配
一般的 Web 服务器有非常多的 URL 绑定,不同的 URL 对应不同的处理器。但是服务器是怎么决定使用哪个处理器的呢?例如,我们现在绑定了 3 个 URL,/和/hello和/hello/world。
显然,如果请求的 URL 为/,则调用/对应的处理器。如果请求的 URL 为/hello,则调用/hello对应的处理器。如果请求的 URL 为/hello/world,则调用/hello/world对应的处理器。
但是,如果请求的是/hello/others,那么使用哪一个处理器呢? 匹配遵循以下规则:
- 首先,精确匹配。即查找是否有
/hello/others对应的处理器。如果有,则查找结束。如果没有,执行下一步; - 将路径中最后一个部分去掉,再次查找。即查找
/hello/对应的处理器。如果有,则查找结束。如果没有,继续执行这一步。即查找/对应的处理器。
这里有一个注意点,如果注册的 URL 不是以/结尾的,那么它只能精确匹配请求的 URL。反之,即使请求的 URL 只有前缀与被绑定的 URL 相同,ServeMux也认为它们是匹配的。
这也是为什么上面步骤进行到/hello/时,不能匹配/hello的原因。因为/hello不以/结尾,必须要精确匹配。
如果,我们绑定的 URL 为/hello/,那么当服务器找不到与/hello/others完全匹配的处理器时,就会退而求其次,开始寻找能够与/hello/匹配的处理器。
看下面的代码:
package main
import (
"fmt"
"log"
"net/http"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the index page")
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the hello page")
}
func worldHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the world page")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/hello/world", worldHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
- 浏览器请求
localhost:8080/将返回"This is the index page",因为/精确匹配;

- 浏览器请求
localhost:8080/hello将返回"This is the hello page",因为/hello精确匹配;

- 浏览器请求
localhost:8080/hello/将返回"This is the index page"。注意这里不是hello,因为绑定的/hello需要精确匹配,而请求的/hello/不能与之精确匹配。故而向上查找到/;

- 浏览器请求
localhost:8080/hello/world将返回"This is the world page",因为/hello/world精确匹配;

- 浏览器请求
localhost:8080/hello/world/将返回"This is the index page",查找步骤为/hello/world/(不能与/hello/world精确匹配)->/hello/(不能与/hello/精确匹配)->/;

- 浏览器请求
localhost:8080/hello/other将返回"This is the index page",查找步骤为/hello/others->/hello/(不能与/hello精确匹配)->/;

如果注册时,将/hello改为/hello/,那么请求localhost:8080/hello/和localhost:8080/hello/world/都将返回"This is the hello page"。自己试试吧!
思考:
使用/hello/注册处理器时,localhost:8080/hello/返回什么?
总结
本文介绍了 Go Web 程序的基本结构。Go Web 的基本形式如下:
package main
import (
"fmt"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World")
}
type greetingHandler struct {
Name string
}
func (h greetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s", h.Name)
}
func main() {
mux := http.NewServeMux()
// 注册处理器函数
mux.HandleFunc("/hello", helloHandler)
// 注册处理器
mux.Handle("/greeting/golang", greetingHandler{Name: "Golang"})
server := &http.Server {
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
后续文章中大部分程序只是在此基础上增加处理器或处理器函数并注册到相应的 URL 中而已。处理器和处理器函数可以只使用一种或两者都使用。注意,为了方便,命名中我都加上了Handler。
参考资料
我
欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

本文由博客一文多发平台 OpenWrite 发布!
Go Web 编程之 程序结构的更多相关文章
- 微信小程序 - 结构目录 | 配置介绍
结构目录 小程序框架提供了自己的视图层描述语言 WXML 和 WXSS,以及 JavaScript,并在视图层与逻辑层间提供了数据传输和事件系统,让开发者能够专注于数据与逻辑. 一.小程序文件结构和传 ...
- 微信小程序结构目录、配置介绍、视图层(数据绑定,运算,列表渲染,条件渲染)
目录 一.小程序结构目录 1.1 小程序文件结构和传统web对比 1.2 基本的项目目录 二.配置介绍 2.1 配置介绍 2.2 全局配置app.json 2.3 page.json 三.视图层 3. ...
- 了解ASP.NET5 Web应用程序结构
本文参考ASP.NET5 官方文档 Understanding ASP.NET 5 Web Apps,加入了一些个人理解,理解不对的地方希望大家能指出,互相学习. ASP.NET 5 针对WEB编程引 ...
- [ Python ] Flask 基于 Web开发 大型程序的结构实例解析
作为一个编程入门新手,Flask是我接触到的第一个Web框架.想要深入学习,就从<FlaskWeb开发:基于Python的Web应用开发实战>这本书入手,本书由于是翻译过来的中文版,理解起 ...
- python3编程基础之一:程序结构
程序从程序入口进入,到程序执行结束,大体是按照顺序结构执行语句.函数或代码块,掌握程序的结构,有利于把握程序的主体框架. 1.顺序结构--最常见的结构 顺序结构的程序设计是最简单的,只要按照解决问题的 ...
- python web编程-概念预热篇
互联网正在引发一场革命??不喜欢看概念的跳过,注意这里仅仅是一些从python核心编程一书的摘抄 这正是最激动人心的一部分了,web编程 Web 客户端和服务器端交互使用的“语言”,Web 交互的标准 ...
- 全部编程皆为Web编程
原文作者:Jeff Atwood 原文链接:http://blog.codinghorror.com/all-programming-is-web-programming Michael Brau ...
- Python 四大主流 Web 编程框架
Python 四大主流 Web 编程框架 目前Python的网络编程框架已经多达几十个,逐个学习它们显然不现实.但这些框架在系统架构和运行环境中有很多共通之处,本文带领读者学习基于Python网络框架 ...
- 清晰架构(Clean Architecture)的Go微服务: 程序结构
我使用Go和gRPC创建了一个微服务,并试图找出最佳的程序结构,它可以用作我未来程序的模板. 我有Java背景,并发现自己在Java和Go之间挣扎,它们之间的编程理念完全不同.我写了一系列关于在项目工 ...
随机推荐
- jq杂项方法/工具方法----isArray()
https://www.cnblogs.com/sandraryan/ $.isArray()函数用于判断指定参数是否是一个数组.返回布尔值. <!DOCTYPE html> <ht ...
- Python--day19--time模块
时间戳转化成结构化时间: 本地时间转换成时间字符串: 时间模块 和时间有关系的我们就要用到时间模块.在使用模块之前,应该首先导入这个模块. #常用方法 1.time.sleep(secs) (线程)推 ...
- 最短路径Dijkstra算法和Floyd算法整理、
转载自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html 最短路径—Dijkstra算法和Floyd算法 Dijks ...
- 2019-1-16-git-subtree-pull-错误-Working-tree-has-modifications
title author date CreateTime categories git subtree pull 错误 Working tree has modifications lindexi 2 ...
- [转]Jmeter压力测试工具安装及使用教程
一.Jmeter下载 进入官网:http://jmeter.apache.org/ 1.第一步进入官网如下图 2.选择进行下载,下载下来为一个压缩包,解压即可. 3.我下载的是jmeter4.0版本, ...
- Python--day28--set去重
set去重:set依赖对象hash eq
- 用一维数组实现栈(C++编程思想 p120)
1 实现思路 向栈中插入4个元素后的状态 执行过程分析: 2 代码实现 clib.h 接口定义 typedef struct CStashTag { int ele_size; //栈中每个元素的占用 ...
- 深入理解 Embedding层的本质
继上文https://blog.csdn.net/weixin_42078618/article/details/82999906探讨了embedding层的降维效果,时隔一个月,分享一下嵌入层在NP ...
- joinColumns和inverseJoinColumns的用法
最近在工作中使用springside,里面用到了hibernate的多对多 一开始我在配置department(部门表)和menu(栏目表)的时候,这样写的. Department实体类中的getMe ...
- [转]Jquery属性选择器(同时匹配多个条件,与或非)(附样例)
1. 前言 为了处理除了两项不符合条件外的选择,需要用到jquery选择器的多个条件匹配来处理,然后整理了一下相关的与或非的条件及其组合. 作为笔记记录. 2. 代码 1 2 3 4 5 6 7 8 ...