Golang ServeMux 是如何实现多路处理的
之前出于好奇看了一下 Golang net/http
包下的部分源码,今天想还是总结一下吧。由于是第一次写文章且抱着忐忑的心情发表,可能有些语义上的不清楚,谅解一下,或者提出修改的建议!
简介
net/http
包里的 server.go
文件里注释写着:ServeMux is an HTTP request multiplexer. 即 ServeMux 是一个 HTTP 请求的 “多路处理器”,因为 ServeMux 实现的功能就是将收到的 HTTP 请求的 URL 与注册的路由相匹配,选择匹配度最高的路由的处理函数来处理该请求。
最简单的栗子:
mux := http.NewServeMux() mux.HandleFunc("/a/b", ab) mux.HandleFunc("/a", a) http.ListenAndServe(":8000", mux) 复制代码
每个路由对应了一个处理函数。
先来看看 NewServeMux
函数
func NewServeMux() *ServeMux { return new(ServeMux) } 复制代码
我们知道 new
函数会为传入的类型分配空间并返回指向该空间首地址的指针,于是我们就获取了一个 ServeMux 实例。
源码分析
ServeMux 结构体
接下来就是 ServeMux 的结构
type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry hosts bool // 标记路由中是否带有主机名 } 复制代码
其中 m
就是用来存储路由与处理函数映射关系的 map, es
按照路由长度从大到小的存放处理函数 (后面会讲为什么要这样),但 ServeMux 为了方便,map存放的值其实是放有处理函数和路由路径的 muxEntry
结构体:
type muxEntry struct { h Handler // 处理函数 pattern string // 路由路径 } 复制代码
ServeMux 暴露的方法主要是下面 4 个:
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) 复制代码
Handle 方法
Handle
方法通过将传入的路由和处理函数存入 ServeMux 的映射表 m
中来实现 “路由注册(register)”
源码具体实现如下:
func (mux *ServeMux) Handle(pattern string, handler Handler) { mux.mu.Lock() defer mux.mu.Unlock() // 检查路由路径是否为空 if pattern == "" { panic("http: invalid pattern") } // 检查处理函数是否为空 if handler == nil { panic("http: nil handler") } // 检查该路由是否已经注册过 if _, exist := mux.m[pattern]; exist { panic("http: multiple registrations for " + pattern) } // 如果还没有任何路由注册,就为 mux.m 分配空间 if mux.m == nil { mux.m = make(map[string]muxEntry) } // 实例化一个 muxEntry e := muxEntry{h: handler, pattern: pattern} // 将该路由与该 muxEntry 的实例存到 mux.m 中 mux.m[pattern] = e // 如果该路由路径以 "/" 结尾,就把该路由按照大到小的路径长度插入到 mux.e 中 if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) } // 如果该路由路径不以 "/" 开始,标记该 mux 中有路由的路径带有主机名 if pattern[0] != '/' { mux.hosts = true } } 复制代码
HandleFunc 方法
HandleFunc
方法接收一个具体的处理函数将其包装成 Handler:
type HandlerFunc func(ResponseWriter, *Request) func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { if handler == nil { panic("http: nil handler") } mux.Handle(pattern, HandlerFunc(handler)) } 复制代码
其中 HandlerFunc(f)
起到的作用就是在 HandlerFunc 中执行 f
Handler 方法
Handler
方法从传入的请求(Request)中拿到 URL 进行匹配, 返回对应的处理函数和路由
在看 Handler
的实现前,先看看它调用的 handler
方法:
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { mux.mu.RLock() defer mux.mu.RUnlock() // 若当前 mux 中注册有带主机名的路由,就用"主机名+路由路径"去匹配 // 也就是说带主机名的路由优先于不带的 if mux.hosts { h, pattern = mux.match(host + path) } // 所以若没有匹配到,就直接把路由路径拿去匹配 if h == nil { h, pattern = mux.match(path) } // 若都没有匹配到,就默认返回 NotFoundHandler,该 Handler 会往 // 响应里写上 "404 page not found" if h == nil { h, pattern = NotFoundHandler(), "" } // 返回获得的 Handler 和路由路径 return } 复制代码
好了,现在是 Handler
方法
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { // 去掉主机名上的端口号 host := stripHostPort(r.Host) // 整理 URL,去掉 ".", ".." path := cleanPath(r.URL.Path) // redirectToPathSlash 在 mux.m 中查看 path+"/" 是否存在 // 如果存在,RedirectHandler 就将该请求重定向到 path+"/" if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok { return RedirectHandler(u.String(), StatusMovedPermanently), u.Path } // 如果整理后的 URL 与请求中的路径不一样,先调用 handler 进行匹配 // 在将请求里的 URL 改成整理后的 URL // 最后将该请求重定向到整理后的 URL if path != r.URL.Path { _, pattern = mux.handler(host, path) url := *r.URL url.Path = path return RedirectHandler(url.String(), StatusMovedPermanently), pattern } // 若以上条件都不满足则返回匹配结果 return mux.handler(host, r.URL.Path) } 复制代码
我们有必要看看 match
方法是怎么进行匹配的
func (mux *ServeMux) match(path string) (h Handler, pattern string) { // 若 mux.m 中已存在该路由映射,直接返回该路由的 Handler,和路径 v, ok := mux.m[path] if ok { return v.h, v.pattern } // 找到路径能最长匹配的路由。 for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, "" } 复制代码
注意这里是在 mux.es 中进行查找,而不是映射表 mux.m 中,而 mux.es 是存放所有以 “/” 结尾的路由路径的切片。因为只会在以 “/” 结尾的路由路径中才会出现需要选择最长匹配方案
比如注册的路由有
mux.HandleFunc("/a/b/", ab) mux.HandleFunc("/a/", a) 复制代码
那么当一个请求的 URL 为 /a/b/c
的时候,我们希望是由 ab 来处理这个请求。
另外,为了减少在 mux.es 中的查询时间, mux.es 中元素是按照它们的长度由大到小顺序存放的。
ServeHTTP 方法
我们知道在 Go 中要实现一个处理请求的 handler 结构体需要让该结构体现实 Handler
接口的 ServeHTTP
方法:
// Handler 接口 type Handler interface { ServeHTTP(ResponseWriter, *Request) } type myHandler struct {} func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("This message is from myHandler.")) } func main() { http.Handle("/", &helloHandler{}) // 路由注册 } 复制代码
我们已经通过 Handler
方法拿到了请求(Request)和它对应的处理函数(Handler)
我们的 ServeMux 是一个结构体,它的 ServeHTTP
方法要做的就是将每个请求派遣(dispatch)到它们对应的处理函数上。
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { // 如果请求路径为 "*",告诉浏览器该连接已关闭并返回状态码 400 if r.RequestURI == "*" { if r.ProtoAtLeast(1, 1) { w.Header().Set("Connection", "close") } w.WriteHeader(StatusBadRequest) return } // 调用 mux.Handler 方法获取请求和它对应的处理函数 h, _ := mux.Handler(r) // 将 ResponseWriter 和 *Request 类型的参数传给处理函数 h.ServeHTTP(w, r) } 复制代码
这样,每收到一个请求就会调用对应的处理函数来处理该请求了。
关于 http.HandleFunc 方法
但我们通常会看到,一些简单的示例代码是下面这样写的:
func helloHandler(w http.ResponseWriter, req *http.Request) { io.WriteString(w, "hello, world!\n") } func main() { http.HandleFunc("/", helloHandler) http.ListenAndServe(":8000", nil) } 复制代码
我们可以看一下 http.HandleFunc
方法做了些什么:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) } 复制代码
可以看到该方法中使用了一个 DefaultServeMux
来注册传入的路由,继续看:
var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux 复制代码
可以看到, http.HandleFunc
也是通过实例化一个全局的 ServeMux
来进行路由注册的。
总结
我们已经了解了 ServeMux 是怎么实现多路处理了,简单概括一下。 Handle
和 HandleFunc
方法用来将路由路径与处理函数的映射通过一个 map 记录到当前的 mux 实例里; Handler
方法将接收的请求中的 URL 预处理后拿去和记录的映射匹配,若匹配到,就返回该路由的处理函数和路径; ServeHTTP
方法将请求派遣给匹配到的处理函数处理。
但是 ServeMux 的多路处理实现并不支持 请求方法判断 ,也不能处理 路由嵌套 和 URL变量值提取 的功能
所以最近在分析 gin 、 kratos/blademaster 这样的框架是如何实现这三个功能的,希望后面能有第二篇总结出现吧。。

其实写到一半的时候发现掘金上已经有 这部分的源码分析 了,于是就看了一下,和自己想的差不多,哈哈