Go 语言路由映射和请求分发的底层实现及自定义路由器
引子
从这一篇教程起,我们将从自定义路由开始探索 Go Web 编程之旅。
开始之前,我们还是回顾下创建第一个 Web 应用中的示例代码:
http.HandleFunc("/", sayHelloWorld)
err := http.ListenAndServe(":9091", nil)
我们在上篇教程介绍过这段代码的底层实现,这里 http.ListenAndServe
方法第二个参数传入的是 nil
,表示底层会使用默认的 DefaultServeMux
实现将上述 HandleFunc
方法传入的处理函数转化为类似 Laravel 框架中基于闭包方式定义的路由:
如上篇教程所言,如果我们想要实现自定义的路由处理器,则需要构建一个自定义的、实现了 Handler
接口的类实例作为 http.ListenAndServe
的第二个参数传入。
在开始介绍自定义路由处理器实现之前,我们先来看看 DefaultServeMux
是如何保存路由映射规则以及分发请求做路由匹配的。
DefaultServeMux 底层实现
顾名思义,DefaultServeMux
是 ServeMux
的默认实例:
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
这里的后缀 Mux
是 Multiplexer 的缩写,ServeMux 可以看作是 HTTP 请求的多路复用器,如果你对这个名词感到陌生,不妨将其类比为 Laravel 框架中的路由器。它们要实现的功能是一致的:接受 HTTP 请求,然后基于映射规则将其转发给正确的处理器进行处理。
那么在 Go Web 应用中,这些路由映射规则是怎么定义的呢?
路由映射规则保存
首先我们来看一下 ServeMux
的数据结构:
type ServeMux struct {
mu sync.RWMutex. // 由于请求涉及到并发处理,因此这里需要一个锁机制
m map[string]muxEntry // 路由规则字典,存放 URL 路径与处理器的映射关系
es []muxEntry // MuxEntry 切片(按照最长到最短排序)
hosts bool // 路由规则中是否包含 host 信息
}
这里,我们需要重点关注的是 muxEntry
结构:
type muxEntry struct {
h Handler // 处理器具体实现
pattern string // 模式匹配字符串
}
最后我们来看一下 Handler
的定义,这是一个接口:
type Handler interface {
ServeHTTP(ResponseWriter, *Request) // 路由处理实现方法
}
当请求路径与 pattern
匹配时,就会调用 Handler
的 ServeHTTP
方法来处理请求。
以我们之前编写的示例应用为例,就是将 URL 路径为 /
的请求转发到 sayHelloWorld
进行处理:
http.HandleFunc("/", sayHelloWorld)
不过 sayHelloWorld
只是一个函数,并没有实现 Handler
接口,之所以可以成功添加到路由映射规则,是因为在底层通过 HandlerFunc()
函数将其强制转化为了 HandlerFunc
类型,而 HandlerFunc
类型实现了 ServeHTTP
方法,这样,sayHelloWorld
方法也就变相实现了 Handler
接口:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
...
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
对于 sayHelloWorld
方法来说,它已然变成了 HandlerFunc
类型的函数类型,当我们在其实例上调用 ServeHTTP
方法时,调用的是 sayHelloWorld
方法本身。
前面我们提到,DefaultServeMux
是 ServeMux
的默认实例,当我们在 HandleFunc
中调用 mux.Handle
方法时,实际上是将其路由映射规则保存到 DefaultServeMux
路由处理器的数据结构中:
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)
}
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
还是以 sayHelloWorld
为例,这里的 pattern
字符串对应的是请求路径 /
,handler
对应的是 sayHelloWorld
函数。
请求分发与路由匹配
保存好路由映射规则之后,客户端请求又是怎么分发的呢?或者说请求 URL 与 DefaultServeMux
中保存的路由映射规则是如何匹配的呢?
我们在上篇教程介绍过,处理客户端请求时,会调用默认 ServeMux
实现的 ServeHTTP
方法:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
w.Header().Set("Connection", "close")
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
如上所示,路由处理器接收到请求之后,如果 URL 路径是 *
,则关闭连接,否则调用 mux.Handler(r)
返回对应请求路径匹配的处理器,然后执行 h.ServeHTTP(w, r)
,也就是调用对应路由 handler
的 ServerHTTP
方法,以 /
路由为例,调用的就是 sayHelloWorld
函数本身。
至于 mux.Handler(r)
的底层匹配实现,感兴趣的同学可以去 net/http
包中查看对应的底层源码,这里就不详细展开了。
通过上面的介绍,我们了解了基于 DefaultServeMux
实现的整个路由规则存储(Web 应用启动期间进行)和请求匹配过程(客户端发起请求时进行),下面我们来看一下如何实现自定义的 路由处理器。
自定义路由处理器
如果你搞清楚了上面的默认实现,编写自定义的路由处理器就会非常简单,我们只需要定义一个实现了 Handler
接口的类,然后将其实例传递给 http.ListenAndServe
方法即可:
package main
import (
"fmt"
"net/http"
)
type MyHander struct {
}
func (handler *MyHander) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
sayHelloGolang(w, r)
return
}
http.NotFound(w, r)
return
}
func sayHelloGolang(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello Golang!")
}
func main() {
handler := MyHander{}
http.ListenAndServe(":9091", &handler)
}
我们运行 go run router.go
来启动这个应用,然后在浏览器中就可以访问 /
路由了:
这个实现很简单,而且我们并没有在应用启动期间初始化路由映射规则,而是在应用启动之后根据请求参数动态判断来做分发的,这样做会影响性能,而且非常不灵活,我们可以通过定义多个处理器的方式来解决这个问题:
package main
import (
"fmt"
"net/http"
)
type HelloHander struct {
}
func (handler *HelloHander) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sayHelloGolang(w, r)
}
type WorldHander struct {
}
func (handler *WorldHander) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
func sayHelloGolang(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello Golang!")
}
func main() {
hello := HelloHander{}
world := WorldHander{}
server := http.Server{
Addr: ":9091",
}
http.Handle("/hello", &hello)
http.Handle("/world", &world)
server.ListenAndServe()
}
只是,我们又回到了老路子上,这里没有显式传入 handler,所以底层依然使用的是 DefaultServeMux
那套路由映射与请求分发机制,要实现完全自定义的、功能更加强大的处理器,只能通过自定义 ServeMux 来实现了,不过在这个领域,已经有非常好的第三方轮子可以直接拿来用了,比如 gorilla/mux 就是其中之一,后续教程我们都将使用它作为路由器,下篇教程我们将简单介绍它的基本使用。
1 条评论
学院君期待你的go教程