基于 Go 的泛型快速实现一个功能完备的路由
Go 语言的路由库有很多,功能上都大同小异,最大的差异应该是路由函数的签名,官方采用了 http.Handler
接口,而大部分非官方路由都将 http.ResponseWriter
和 http.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/v9"
4
5type (
6 Router = mux.Router[http.Handler]
7 Prefix = mux.Prefix[http.Handler]
8 Resource = mux.Resource[http.Handler]
9 Middleware = mux.Middleware[http.Handler]
10)
定义 New 函数
我们需要一个 CallFunc
函数,用于将给定的参数转换成调用 http.Handler
的方法。其原型如下:
1CallFunc[T any] func(http.ResponseWriter, *http.Request, types.Route, T)
New
可以直接调用 NewRouter
方法,给出 CallFunc
的实例化方法即可。
1package custom_router
2
3import "github.com/issue9/mux/v9"
4
5func call(w http.ResponseWriter, r *http.Request, ps types.Route, h http.Handler) {
6 h.ServeHTTP(w, WithValue(r, ps))
7}
8
9func New(name string, o *mux.Options) *Router {
10 return NewRouter[http.Handler](name, call, o)
11}
辅助函数
然后定义一些辅助函数,比如将参数写入到 http.Request
和从 http.Request
中获取参数。
1package custom_router
2
3import "github.com/issue9/mux/v9"
4
5type contextKey int
6
7const contextKeyParams contextKey = 0
8
9// GetParams 获取当前请求实例上的参数列表
10func GetParams(r *http.Request) types.Route {
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 types.Route) *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("", &mux.Options{})
5 r.Get("/users/{id:\\d+}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
6 // TODO
7 }))
8}
一个更复杂的例子
下面定义了一个新路由,摒弃了官方的 http.ResponseWriter
和 http.Request
,采用 Context
对象传递上下文所需的内容。所以我们自定义了一个 Handler
接口用以代替官方的 http.Handler
接口,其 Handle
方法的参数只接收 Context
对象。
对于 New
方法也不再是直接传递 *options
对象,而是以可选的函数方法的形式传递。
1package custom_router
2
3import "github.com/issue9/mux/v9"
4
5type (
6 Context struct {
7 R *http.Request
8 W http.ResponseWriter
9 P types.Route
10 }
11
12 Handler interface {
13 Handle(*Context)
14 }
15
16 HandlerFunc func(*Context)
17
18 Router = mux.Router[Handler]
19 Prefix = mux.Prefix[Handler]
20 Resource = mux.Resource[Handler]
21 Middleware = mux.Middleware[Handler]
22
23 Option func(o *mux.Options)
24)
25
26func (f HandlerFunc) Handle(c *Context) { f(c) }
27
28func call(w http.ResponseWriter, r *http.Request, ps types.Route, h Handler) {
29 h.Handle(&Context{R: r, W: w, P: ps})
30}
31
32func New(name string, o ...Option) *Router {
33 opt := &options{}
34 for _, oo := range o {
35 oo(opt)
36 }
37 return NewRouter[Handler](name, call, opt)
38}
39
40// 一些实现 Option 的函数,整个 options 的内容都可以采用此方式设置。
41
42func Lock(o *options) {
43 o.Lock = true
44}
45
46func Unlock(o *options) {
47 o.Lock = false
48}
49
50func NotFound(f http.HandlerFunc) Option {
51 if f == nil {
52 f = http.NotFound
53 }
54 return func(o *mux.Options) {
55 o.NotFound = f
56 }
57}
之后就可以正常使用路由:
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
的性能测试。