从这一篇教程起,我们将从自定义路由器开始探索 Go Web 编程之旅。
开始之前,我们还是回顾下创建第一个 Web 应用中的示例代码:
http.HandleFunc("/", sayHelloWorld)
err := http.ListenAndServe(":9091", nil)我们在上篇教程介绍过这段代码的底层实现,这里 http.ListenAndServe 方法第二个参数传入的是 nil,表示底层会使用默认的 DefaultServeMux 实现将上述 HandleFunc 方法传入的处理函数转化为基于闭包方式定义的路由:
如上篇教程所言,如果我们想要实现自定义的路由处理器,则需要构建一个自定义的、实现了 Handler 接口的类实例作为 http.ListenAndServe 的第二个参数传入。
在开始介绍自定义路由处理器实现之前,我们先来看看 DefaultServeMux 是如何保存路由映射规则以及分发请求做路由匹配的。
DefaultServeMux 所属的类型是 ServeMux:
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux首先我们来看一下 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) // 路由处理实现方法
}我们之前定义的 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.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 来启动这个应用,然后在浏览器中就可以访问 / 路由了:
这个实现很简单,而且我们并没有在应用启动期间初始化路由映射规则,而是在应用启动之后根据请求参数动态判断来做分发的,这样做会影响性能,而且非常不灵活,我们当然可以仿照 DefaultServeMux 来实现自定义的 ServeMux,不过已经有非常好的第三方轮子可以直接拿来用了,比如 gorilla/mux,后续教程我们都将使用它作为路由器,下篇教程我们将简单介绍它的基本使用。