Go 源码学习 —— Chi

Go 源码学习 —— Chi

目录

  • 基本使用
    • 1创建 Mux
    • 2注册http 方法
    • 3 作为Handler 传入Server
    • 4 监听请求
  • REST 接口
    • json
  • 中间件 middlleware
    • 1 注册
    • 2构建调用链
  • 路由 router
    • 1 单一路由
    • 2 子路由

image_tXTfnigNJl.png

  • 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()

    初始化了muxtreepool, 其中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.handlerServeHTTP 方法, 而此时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 注册

  • muxmiddlewares 切片中, 这里有个细节是, 添加中间件要在实例化mx.handler之前,别忘了他是在mx.handle 中, 借助mx.updateRouteHandler() 调用的 , 当然mx.inlinetrue 时会更新它
    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 请求链接通过contextmiddlewares 传递

image_QrMXuaPvOU.png

路由 router

node.InsertRoute()前面有提过, 这里就是建立methodpatternhandler映射的地方, 还记得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 包括handlerpatternparamKeys

image_Hb_mnl-553.png

  • 前面这么多, 都只是建立映射和设置叶子结点, 路径解析是如何做到的呢?同样是在node.findRoute 方法中递归.

2 子路由

  • 官网上提供了两种方式的子路, 分别是mux.Mount()mux.Route(), 首先看一下mux.Mount().Mount() 会分别把patternpattern+"/"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
    }