likes
comments
collection
share

Go 源码学习 —— Chi

作者站长头像
站长
· 阅读数 18

Go 源码学习 —— Chi

目录

Go 源码学习 —— Chi

  • 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 传递

Go 源码学习 —— Chi

路由 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

Go 源码学习 —— Chi

  • 前面这么多, 都只是建立映射和设置叶子结点, 路径解析是如何做到的呢?同样是在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
    }