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
是缓存context
func 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
的主线流程, 虽然这个框架简单, 但是不至于就这些而已, 我们看看还有哪些可以学些的内容
rest
middleware
router
REST 接口
现在基本所有的web
框架都提供了REST
支持了, 这对于前后端分离的项目下很方便, chi
在核心库中直接提供各种http
方法, 甚至可以自定义方法, 但是还有一些方法需要通过中间件实现
json
rest
并没有规定一定是json
传送方式, 但是json
同样是web
框架必备的数据传递方式之一.chi
中使用json
请求数据, 可以通过render
库实现, 这里render
- 首先通过中间件, 设置请求头的
content-type
r.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
添加children
node.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 }