Go 源码学习 —— Chi
Go 源码学习 —— Chi
目录
- 基本使用
- 1创建 Mux
- 2注册http 方法
- 3 作为Handler 传入Server
- 4 监听请求
- REST 接口
- json
- 中间件 middlleware
- 1 注册
- 2构建调用链
- 路由 router
- 1 单一路由
- 2 子路由

- CHI 是一个http 路由器, 轻量级低于1000 LOC
- 没有额外的库
基本使用
func main() {
r := chi.NewRouter()
// 统一把Get的第二个参数表述为 handleFunc
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!n"))
})
http.ListenAndServe(":3000", r)
}
1 创建 Mux
-
chi::NewRouter() -
mux::NewMux()初始化了
mux的tree和pool, 其中pool是缓存contextfunc NewMux() *Mux { mux := &Mux{tree: &node{}, pool: &sync.Pool{}} mux.pool.New = func() interface{} { return NewRouteContext() } return mux }
2 注册http 方法
- 所有的请求最终调用
mux.handle方法, 其中mx.updateRouteHandler()构建了调用链, 这个在middleware有详细描述, 这里是给mx.handle赋值我们自定义的handleFunc, 也就是注册http方法中的第二个参数.func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { // Build the computed routing handler for this routing pattern. if !mx.inline && mx.handler == nil { mx.updateRouteHandler() } // ... return mx.tree.InsertRoute(method, pattern, h) } mx.updateRouteHandler()构建调用链, 当没有任何middlewares时,mx.handler就是mx.routeHTTP,表示路由分发func (mx *Mux) updateRouteHandler() { // chain 第二个参数是 http.Handler mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) }node.InsertRoute()内容较多,存在一个无限循环, 会构建一个routing tree,当前例子中只有一个路由, 因此返回值只有一个节点不作多的解释, 更具体的描述在router一节讲解
3 作为Handler 传入Server
- 所以整个
chi是在Handler上做文章
4 监听请求
- 因为
mux实现了Handler接口, 所有当http请求到来时, 会执行mux.ServeHTTP, 这里也主要做了两件事, 一是操作context, 如果同一请求链, 则直接返回, 否则从缓存池中取一个, 并装载到Request上, 二是执行mx.handler的ServeHTTP方法, 而此时mx.handler就是我们定义的handleFunc方法, 所以执行具体定义的方法func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { if mx.handler == nil { mx.NotFoundHandler().ServeHTTP(w, r) return } // 已有上下文, 直接执行 rctx, _ := r.Context().Value(RouteCtxKey).(*Context) if rctx != nil { mx.handler.ServeHTTP(w, r) return } // context 缓存 rctx = mx.pool.Get().(*Context) rctx.Reset() rctx.Routes = mx rctx.parentCtx = r.Context() // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) // 重点 mx.handler.ServeHTTP(w, r) mx.pool.Put(rctx) } - 这里有必要知道
mx.handler, 当没有middlewares, 它就是mx.routeHTTP, 有middlewares会提前执行相应的middlewares, 这里通过routePath找到对应的http.Handler, 执行相应的接口实现func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { // Grab the route context object rctx := r.Context().Value(RouteCtxKey).(*Context) // The request routing path routePath := rctx.RoutePath if routePath == "" { if r.URL.RawPath != "" { routePath = r.URL.RawPath } else { routePath = r.URL.Path } if routePath == "" { routePath = "/" } } // Check if method is supported by chi if rctx.RouteMethod == "" { rctx.RouteMethod = r.Method } method, ok := methodMap[rctx.RouteMethod] if !ok { mx.MethodNotAllowedHandler().ServeHTTP(w, r) return } // 节点查找 if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { h.ServeHTTP(w, r) return } if rctx.methodNotAllowed { mx.MethodNotAllowedHandler(rctx.methodsAllowed...).ServeHTTP(w, r) } else { mx.NotFoundHandler().ServeHTTP(w, r) } }
OK, 到目前为止我们走完了CHI 的主线流程, 虽然这个框架简单, 但是不至于就这些而已, 我们看看还有哪些可以学些的内容
restmiddlewarerouter
REST 接口
现在基本所有的web框架都提供了REST支持了, 这对于前后端分离的项目下很方便, chi 在核心库中直接提供各种http 方法, 甚至可以自定义方法, 但是还有一些方法需要通过中间件实现
json
rest 并没有规定一定是json传送方式, 但是json 同样是web 框架必备的数据传递方式之一.chi 中使用json 请求数据, 可以通过render 库实现, 这里render
- 首先通过中间件, 设置请求头的
content-typer.Use(render.SetContentType(render.ContentTypeJSON)) - 返回参数实现
Renderer接口 - 然后通过
render.RenderList或者render.Render返回, 核心是 判断合法性的renderer方法 数据分流的DefaultResponder方法, 这里JSON方法最终还是会调用w.Write, 但是入参是经过json编码的
中间件 middlleware
1 注册
- 到
mux的middlewares切片中, 这里有个细节是, 添加中间件要在实例化mx.handler之前,别忘了他是在mx.handle中, 借助mx.updateRouteHandler()调用的 , 当然mx.inline为true时会更新它func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { if mx.handler != nil { panic("chi: all middlewares must be defined before routes on a mux") } mx.middlewares = append(mx.middlewares, middlewares...) }
2构建调用链
- 在基本使用的步骤2 中有个函数没有展开, 那就是
mx.updateRouteHandler(), 当时因为middlewares没有值, 所以直接把自定义的handleFunc返回给mx.handler. 当middlewares有值时, 知识点就来了. 这里先取到middlewares的最后一个值, 然后调用他func (mx *Mux) updateRouteHandler() { // http.HandlerFunc(mx.routeHTTP) 是类型转换不是函数调用 mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) }func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { if len(middlewares) == 0 { return endpoint } // 调用具体的 middlewares 元素 h := middlewares[len(middlewares)-1](endpoint) for i := len(middlewares) - 2; i >= 0; i-- { h = middlewares[i](h) } return h }- 调用具体
middlewares元素, 这里以middleware.Logger为例.middlewares元素的函数签名为func(http.Handler) http.Handler, 所以middleware的实质是函数闭包.
- 调用具体
logger中关键逻辑是在RequestLogger, 重要的一点RequestLogger是在init中提前执行好的, 所以chain.chain()中h := middlewares[len(middlewares)-1](endpoint)这行代码真正执行的是RequestLogger的返回函数, 这里以闭包的形式把next作为fn的下一步请求操作, 这里就构造了http的请求链// f是配置相关 func RequestLogger(f LogFormatter) func(next http.Handler) http.Handler { // 这里入参 updateRouteHandler() return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { // 所以这里http 请求可以沿着 fn => next entry := f.NewLogEntry(r) ww := NewWrapResponseWriter(w, r.ProtoMajor) t1 := time.Now() defer func() { entry.Write(ww.Status(), ww.BytesWritten(), ww.Header(), time.Since(t1), nil) }() // 执行 Handler 接口 next.ServeHTTP(ww, WithLogEntry(r, entry)) } return http.HandlerFunc(fn) } }- 这里
http请求链接通过context在middlewares传递
- 这里

路由 router
node.InsertRoute()前面有提过, 这里就是建立method、pattern、handler映射的地方, 还记得mux.tree在哪里初始化的不, 对, 就是chi.NewRouter(), 所以这里可以直接调用InsertRoute()方法, 目前所有值都还是初始状态, 我们需要知道查找数据是怎么构建的, 这段逻辑较为复杂, 咱们分情况讨论.
- 单一路由
- 子路由
1 单一路由
- 即只有文本, 比如
r.Get("/articles", getArticle), 最简单的形式, 只有两个节点, 子节点的prefix就是自路径n = n.getEdge(segTyp, label, segTail, prefix) if n == nil { child := &node{label: label, tail: segTail, prefix: search} // 1. child 没有参数 (即 search 没有{ * 之类) // parent.children[ntStatic] append child // 返回值是最后一个节点 hn := parent.addChild(child, search) hn.setEndpoint(method, handler, pattern) return hn } - 有一个查找参数, 比如
r.Get("/articles/{date}-{slug}", getArticle), 有两个路径参数, 这里为了更加清晰表述, 我画了一个图, 为了构建路径树, 通过逐步分解路由, 迭代addChild方法 , 不停的为当前node添加childrennode.children是切片数组不是切片, 正好表示四种类型的节点node.endpoints是一个映射map[methodTyp]*endpoint, 作用是叶子节点上的http映射, 其中endpoint包括handler和pattern、paramKeys

- 前面这么多, 都只是建立映射和设置叶子结点, 路径解析是如何做到的呢?同样是在
node.findRoute方法中递归.
2 子路由
- 官网上提供了两种方式的子路, 分别是
mux.Mount()和mux.Route(), 首先看一下mux.Mount().Mount()会分别把pattern、pattern+"/"、pattern+"*"加到当前叶子节点, 这里有个最长相同字符串的算法... mx.handle(mALL|mSTUB, pattern, mountHandler) mx.handle(mALL|mSTUB, pattern+"/", mountHandler) ... n := mx.handle(method, pattern+"*", mountHandler) mux.Route()其实也是如出一辙, 只不过是在方法内部提前创建Mux然后调用mx.Mount()func (mx *Mux) Route(pattern string, fn func(r Router)) Router { if fn == nil { panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) } subRouter := NewRouter() fn(subRouter) mx.Mount(pattern, subRouter) return subRouter }