基于 Go 的泛型快速实现一个功能完备的路由

作者: caixw
修改时间:

Go 语言的路由库有很多,功能上都大同小异,最大的差异应该是路由函数的签名,官方采用了 http.Handler 接口,而大部分非官方路由都将 http.ResponseWriterhttp.Request 合并成了一个对象。本文介绍的库 https://github.com/issue9/mux 利用 go1.18 对泛型的支持,实现了用户自定义该功能的需求,仅需要几步即可实现一个完善的路由,适用于快速开发一个 web 框架。

如何使用

以实现一个兼容 http.Handler 接口的路由为例,仅需以下几个步骤即可。

定义路由处理类型

可以是接口,也可以是函数,我们以 http.Handler 为例,那么该类型就是 http.Handler 接口,此步可以省略。

定义泛型对应的类型

http.Handler 为参数实例化泛型类型:

 1package custom_router
 2
 3import "github.com/issue9/mux/v6"
 4
 5type (
 6    Router         = mux.RouterOf[http.Handler]
 7    Prefix         = mux.PrefixOf[http.Handler]
 8    Resource       = mux.ResourceOf[http.Handler]
 9    Options        = mux.OptionsOf[http.Handler]
10    Middleware     = mux.MiddlewareOf[http.Handler]
11)

定义 New 函数

我们需要一个 CallOf 函数,用于将给定的参数转换成调用 http.Handler 的方法。其原型如下:

1CallOf[T any] func(http.ResponseWriter, *http.Request, Params, T)

New 可以直接调用 NewRouterOf 方法,给出 CallOf 的实例化方法即可。

 1package custom_router
 2
 3import "github.com/issue9/mux/v6"
 4
 5func call(w http.ResponseWriter, r *http.Request, ps Params, h http.Handler) {
 6    h.ServeHTTP(w, WithValue(r, ps))
 7}
 8
 9func New(name string, o *Options) *Router {
10    return NewRouterOf[http.Handler](name, call, o)
11}

辅助函数

然后定义一些辅助函数,比如将参数写入到 http.Request 和从 http.Request 中获取参数。

 1package custom_router
 2
 3import "github.com/issue9/mux/v6"
 4
 5type contextKey int
 6
 7const contextKeyParams contextKey = 0
 8
 9// GetParams 获取当前请求实例上的参数列表
10func GetParams(r *http.Request) mux.Params {
11    if ps := r.Context().Value(contextKeyParams); ps != nil {
12        return ps.(Params)
13    }
14    return nil
15}
16
17// WithValue 将参数 ps 附加在 r 上
18func WithValue(r *http.Request, ps mux.Params) *http.Request {
19    if ps == nil || ps.Count() == 0 {
20        return r
21    }
22
23    if ps2 := GetParams(r); ps2 != nil && ps2.Count() > 0 {
24        ps2.Range(func(k, v string) {
25            ps.Set(k, v)
26        })
27    }
28
29    return r.WithContext(context.WithValue(r.Context(), contextKeyParams, ps))
30}

这样一个兼容 http.Handler 的路由就完成了,之后就可以正常使用路由。 它支持普通字符串匹配,也支持以 {name:rule} 形式的匹配,其中 rule 可以是正则表达式或空值。具体的语法可以参考 https://github.com/issue9/mux

1package custom_router
2
3func main() {
4    r := New("", &Options{})
5    r.Get("/users/{id:\\d+}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
6        // TODO
7    }))
8}

一个更复杂的例子

下面定义了一个新路由,摒弃了官方的 http.ResponseWriterhttp.Request,采用 Context 对象传递上下文所需的内容。所以我们自定义了一个 Handler 接口用以代替官方的 http.Handler 接口,其 Handle 方法的参数只接收 Context 对象。

对于 New 方法也不再是直接传递 *options 对象,而是以可选的函数方法的形式传递。

 1package custom_router
 2
 3import "github.com/issue9/mux/v6"
 4
 5type (
 6    Context struct {
 7        R *http.Request
 8        W http.ResponseWriter
 9        P mux.Params
10    }
11
12    Handler interface {
13        Handle(*Context)
14    }
15
16    HandlerFunc func(*Context)
17
18    Router         = mux.RouterOf[Handler]
19    Prefix         = mux.PrefixOf[Handler]
20    Resource       = mux.ResourceOf[Handler]
21    Middleware     = mux.MiddlewareOf[Handler]
22
23    options        = mux.OptionsOf[Handler]
24    Option func(o *options)
25)
26
27func (f HandlerFunc) Handle(c *Context) { f(c) }
28
29func call(w http.ResponseWriter, r *http.Request, ps mux.Params, h Handler) {
30    h.Handle(&Context{R: r, W: w, P: ps})
31}
32
33func New(name string, o ...Option) *Router {
34    opt := &options{}
35    for _, oo := range o {
36        oo(opt)
37    }
38    return NewRouterOf[Handler](name, call, opt)
39}
40
41// 一些实现 Option 的函数,整个 options 的内容都可以采用此方式设置。
42
43func Lock(o *options) {
44    o.Lock = true
45}
46
47func Unlock(o *options) {
48    o.Lock = false
49}
50
51func NotFound(f http.HandlerFunc) Option {
52    if f == nil {
53        f = http.NotFound
54    }
55    return func(o *Options) {
56        o.NotFound = f
57    }
58}

之后就可以正常使用路由:

1package custom_router
2
3func main() {
4    r := New("")
5    r.Get("/users/{id:\\d+}", HandlerFunc(func(ctx *Context){
6        // TODO
7    }))
8}

性能

有关性能可以参考 https://caixw.github.io/go-http-routers-testing/ 提供了基于 http.Handler 的性能测试。