Loading... # 0x01 <div class="tip inlineBlock info"> 本篇分析基于 **Go 1.14.12** 与 **github.com/go-chi/chi/v5 v5.0.3** 注意信息时效 </div> [go-chi](https://github.com/go-chi/chi) 是一个使用 Go 实现的非常轻量级的 Web 框架,框架仅使用了几千行代码实现了一个 Web 框架最基础也是最核心的功能 —— 路由(Router)、中间件(middleware)、上下文(context)。我们也就从这几个方面入手分析一下这个框架吧~ # 前置知识 阅读本文必须掌握的前置知识: - [net/http](https://segmentfault.com/a/1190000021653550) - [context](https://segmentfault.com/a/1190000022534841) - [sync.pool](https://www.cnblogs.com/qcrao-2018/p/12736031.html) 在分析代码前,我们先聊一聊什么是 Web 框架、Web 框架能做什么。 Web 框架是一个能够接收网络请求(通常是 HTTP 请求)并进行处理、返回对应数据的软件架构。Web 框架实现了接受请求、根据请求路由派发到具体响应的业务处理函数,再将业务处理函数返回的结果返回给客户端。让使用 Web 框架的程序员们不需要关心底层细节,只需要实现业务处理函数,将其绑定进 Web 框架的路由中,即可完成需求。 > Web框架(Web framework)或者叫做Web应用框架(Web application framework),是用于进行Web开发的一套软件架构。大多数的Web框架提供了一套开发和部署网站的方式。为Web的行为提供了一套支持支持的方法。使用Web框架,很多的业务逻辑外的功能不需要自己再去完善,而是使用框架已有的功能就可以。  而 go-chi,主要就是实现了路由这个部分,并且附带了中间件和上下文两个开发中常用的功能 # Go-chi 源码分析 终于进入正题了,我们先从 chi 官网的例子入手,看看如何使用 chi 完成一个简单的 Web 应用。 ```go package main import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "net/http" ) func main() { r := chi.NewRouter() r.Use(middleware.Logger) r.Get("/", func(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte("Hello world!")) if err != nil { return } }) err := http.ListenAndServe(":9000", r) if err != nil { return } } ``` 从代码逻辑可以简单的看出,我们先定义了一个 `Mux`对象`r`(`chi.NewRouter()`的返回值),然后使用 `r.Use`方法绑定了一个中间件,之后使用 `r.Get` 将一个返回 `hello world` 的方法绑定到 `"/"`上,最后使用`http.ListenAndServe`方法启动了 Web 服务。 `http.ListenAndServe`方法的第二个参数,接受一个实现了`ServeHTTP(ResponseWriter, *Request)`的`Handle`实例对象,并且在每一个请求进入的时候会调用这个对象的 `ServeHTTP` 方法处理请求。具体细节请参考 [深入学习用 Go 编写 HTTP 服务器](https://segmentfault.com/a/1190000021653550)。偷懒的同学可以参考下面的图,大致了解下。 ```go func (srv *Server) Serve(l net.Listener) error { ...... baseCtx := context.Background() // base is always background, per Issue 16220 ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, e := l.Accept()// 接收 listener 过来的网络连接请求 ...... c := srv.newConn(rw) c.setState(c.rwc, StateNew) // 将连接放在 Server.activeConn这个 map 中 go c.serve(ctx)// 创建协程处理请求 } } ```  于此,我们就找到了这次源码分析的关键 `ServeHTTP` ## 中间件(middleware) 先把 `ServeHTTP` 放在一边不管,我们把上面的示例代码启动,一步步 debug 拆解 chi 的内部。首先第一个是 `r.Use(middleware.Logger)` ,通过源码可以看出,逻辑非常的简单,只是把传入的 middleware 函数放入一个 slice 中。 ```go 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...) } ``` 但是这里还看不出这些 middleware 是怎么和请求绑定,但是我们可以通过 middleware 的定义知道它主要是为了在具体处理请求的前后添加一些诸如计算处理时间的功能。那么 middleware 与处理函数肯定是一个闭包的关系。  go-chi 确实也是这么处理的,我们接着往后面 debug 能发现,在建立第一个路由的时候,会调用 `updateRouteHandler()` 方法,而 `updateRouteHandler()` 方法会调用 `chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP))` 将我们之前创建的 middleware 与 endpoint 处理函数进行绑定。 ```go // chain builds a http.Handler composed of an inline middleware stack and endpoint // handler in the order they are passed. func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { // Return ahead of time if there aren't any middlewares for the chain if len(middlewares) == 0 { return endpoint } // Wrap the end handler with the middleware chain h := middlewares[len(middlewares)-1](endpoint) for i := len(middlewares) - 2; i >= 0; i-- { h = middlewares[i](h) } return h } ``` ## 路由(Router) chi 里添加一个路由是调用了 `mx.tree.InsertRoute(method, pattern, h)` 方法,而整个 `tree.go` 文件近千行代码全是处理路由的方法,我们先简化一下。chi 的路由主要是直接魔改了 [julienschmidt/httprouter](https://github.com/julienschmidt/httprouter) 这个 router 项目,而这个项目使用了 radix tree 的数据结构,他是经常刷 LeetCode 的同学们一定知道的前缀树的变体。他将绑定路由时的字符串拆成了下面这种结构,以便于后续路由匹配时的搜索。 ```#bash / /search /support /blog/:post/ /about-us/team/ /contact/ ↑这组路由对应的 httproute 树如下 Priority Path Handle 9 \ *<1> 3 ├s nil 2 |├earch\ *<2> 1 |└upport\ *<3> 2 ├blog\ *<4> 1 | └:post nil 1 | └\ *<5> 2 ├about-us\ *<6> 1 | └team\ *<7> 1 └contact\ *<8> ``` ## 上下文(Context) 上下文没有什么特别好说的,还是沿用了 Go 标准库中的 Context。只是在请求传入的 Context 里新增了处理 urlparam 之类的东西,并且用一个 `sync.pool` 维护自定义的 Context 节省内存。 ## ServeHTTP 终于说到了这个最核心的东西,当 `net/http` 中的 Listener 监听到请求后会直接调用 `serverHandler{c.server}.ServeHTTP(w, w.req)` 方法 ```go type serverHandler struct { srv *Server } func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) } ``` 而这个 `c.server` 就是我们传入 `http.ListenAndServe` 中的第二个参数,即 go-chi 自定义的`Mux`对象`r`(`chi.NewRouter()`的返回值)然后调用 `Mux` 的 `ServeHTTP` 方法。 ```go func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 有关 Context 与 sync.pool 的处理 ... mx.handler.ServeHTTP(w, r) ... } ``` 这里的 `mx.handler` 就是我们前面提到的,在建立第一个路由的时候初始化的 `Handler` 对象 ```go func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { if len(pattern) == 0 || pattern[0] != '/' { panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) } // Build the computed routing handler for this routing pattern. if !mx.inline && mx.handler == nil { // 就是这里~ mx.updateRouteHandler() } // Build endpoint handler with inline middlewares for the route var h http.Handler if mx.inline { mx.handler = http.HandlerFunc(mx.routeHTTP) h = Chain(mx.middlewares...).Handler(handler) } else { h = handler } // Add the endpoint to the tree and return the node return mx.tree.InsertRoute(method, pattern, h) } ``` 绑定中间件也是在这一步完成的,并且每一层中间件都会有个 `next.ServeHTTP()` 确保中间件执行完毕后会正确执行最后一层的 `ServeHTTP()` 。`mx.handler.ServeHTTP(w, r)` 这个函数的代码如下: ```go func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } ``` 根据中间件和这个函数,我们把调用拆解一下,调用关系如下: ```go middleware1(middleware2(ServeHTTP(w, r))) ``` 中间件处理完毕后倒数第二层 `ServeHTTP` 会进而调用 `func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request)` 将请求路由到绑定的 Endpoint 处理函数,再调用 Endpoint 的 `ServeHTTP` 执行。最后再一层一层的返回完成响应。 最后,不得不让人佩服 http 标准库的设计,通过一个 `Interface` 将 Web 框架的基本功能抽象的明明白白。如果你还不明白 `Handler`,`HandlerFunc`和`ServeHTTP`的关系,可以看看曹大的这篇文章[[Go 语言高级编程 - 中间件 - 5.3.2 使用中间件剥离非业务逻辑](https://chai2010.cn/advanced-go-programming-book/ch5-web/ch5-03-middleware.html) # Reference [什么是Web框架、web框架有什么功能?](https://zhuanlan.zhihu.com/p/59841196) [radix tree有哪些用途](https://mp.weixin.qq.com/s/3bKRVdPl-1_NKXARqLvJIw) [Go 语言高级编程 - 第5章 go 和 Web](https://chai2010.cn/advanced-go-programming-book/ch5-web/readme.html) 最后修改:2021 年 07 月 19 日 11 : 21 AM © 允许规范转载 赞赏 如果觉得我的文章对你有用,请随意赞赏 赞赏作者 支付宝微信