路由映射和请求分发的底层实现及自定义路由器

引子

从这一篇教程起,我们将从自定义路由开始探索 Go Web 编程之旅。

开始之前,我们还是回顾下创建第一个 Web 应用中的示例代码:

http.HandleFunc("/", sayHelloWorld)
err := http.ListenAndServe(":9091", nil)

我们在上篇教程介绍过这段代码的底层实现,这里 http.ListenAndServe 方法第二个参数传入的是 nil,表示底层会使用默认的 DefaultServeMux 实现将上述 HandleFunc 方法传入的处理函数转化为类似 Laravel 框架中基于闭包方式定义的路由:

ServeHTTP 方法源码

如上篇教程所言,如果我们想要实现自定义的路由处理器,则需要构建一个自定义的、实现了 Handler 接口的类实例作为 http.ListenAndServe 的第二个参数传入。

在开始介绍自定义路由处理器实现之前,我们先来看看 DefaultServeMux 是如何保存路由映射规则以及分发请求做路由匹配的。

DefaultServeMux 底层实现

顾名思义,DefaultServeMuxServeMux 的默认实例:

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

这里的后缀 Mux 是 Multiplexer 的缩写,ServeMux 可以看作是 HTTP 请求的多路复用器,如果你对这个名词感到陌生,不妨将其类比为 Laravel 框架中的路由器。它们要实现的功能是一致的:接受 HTTP 请求,然后基于映射规则将其转发给正确的处理器进行处理。

ServeMux 路由映射与请求分发原理

那么在 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 匹配时,就会调用 HandlerServeHTTP 方法来处理请求。

以我们之前编写的示例应用为例,就是将 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 方法本身。

前面我们提到,DefaultServeMuxServeMux 的默认实例,当我们在 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),也就是调用对应路由 handlerServerHTTP 方法,以 / 路由为例,调用的就是 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 来启动这个应用,然后在浏览器中就可以访问 / 路由了:

-w553

这个实现很简单,而且我们并没有在应用启动期间初始化路由映射规则,而是在应用启动之后根据请求参数动态判断来做分发的,这样做会影响性能,而且非常不灵活,我们可以通过定义多个处理器的方式来解决这个问题:

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 就是其中之一,后续教程我们都将使用它作为路由器,下篇教程我们将简单介绍它的基本使用。

上一篇: HTTP 请求处理的底层机制

下一篇: 基于 gorilla/mux 包实现路由定义和请求分发:基本使用