找回密码
 立即注册
首页 业界区 安全 Gin 框架核心架构解析

Gin 框架核心架构解析

麓吆 前天 22:36
Gin 是一个用Go (Golang) 编写的HTTP Web 框架,架构非常简洁,得益于Go的net/http库,不用处理http协议底层细节,可以更好的聚焦于应用层逻辑
net/http库概览

Gin是基于net/http实现的, 在介绍Gin之前,不妨先了解下net/http
net/http提供了HTTP客户端和服务端的功能,这里主要关心服务端的功能
服务端核心设计

得益于Go的并发模型,这里并不需要关心像传统处理网络请求的Reactor或Proactor的I/O复用机制,而是为每个请求都创建一个独立的goroutine来处理(对这部分有兴趣可以了解下Go的调度器和goroutine)。
net/http服务端功能的核心设计是http.Hander接口
  1. type Handler interface{
  2.         ServeHTTP(ResponseWriter, *Request)
  3. }
复制代码
任何实现了这个接口的类型都可以作为一个HTTP请求处理器。ServeHTTP 方法接收一个 http.ResponseWriter 和一个 *http.Request,分别用于写入响应和读取请求信息。
这种设计将业务逻辑与底层的网络细节彻底解耦,开发者只需关注如何处理请求和生成响应即可。
流程说明

net/http可以通过调用ListenAndServe监听端口,启动HTTP服务
使用net/http启动HTTP服务的简单示例:
  1. func main() {
  2.     http.Handle("/foo", fooHandler)
  3.         http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
  4.                 fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
  5.         })
  6.         log.Fatal(http.ListenAndServe(":8080", nil))
  7. }
复制代码
ListenAndServe starts an HTTP server with a given address and handler. The handler is usually nil, which means to use DefaultServeMux. Handle and HandleFunc add handlers to DefaultServeMux
流程处理示意图:
graph TD        A[客户端请求] --> B[net/http.ListenAndServe]        B --> C[创建 goroutine]        C --> D[解析 HTTP 请求]         D --> E[调用 Handler.ServeHTTP]         E --> F[业务逻辑处理]         F --> G[返回 HTTP 响应]http.ListenAndServe
  1. // ListenAndServe listens on the TCP network address addr and then calls
  2. // [Serve] with handler to handle requests on incoming connections.
  3. // Accepted connections are configured to enable TCP keep-alives.
  4. //
  5. // The handler is typically nil, in which case [DefaultServeMux] is used.
  6. //
  7. // ListenAndServe always returns a non-nil error.
  8. func ListenAndServe(addr string, handler Handler) error {
  9.     server := &Server{Addr: addr, Handler: handler}
  10.     return server.ListenAndServe()
  11. }
复制代码
ListenAndServe的第二个参数接收的就是前面说的实现了http.Handler接口的类型,默认的DefaultServeMux也是实现了该接口的一个类型,Gin的Engine也是一个实现了http.Handler的类型
DefaultServeMux:
  1. // Handler returns the handler to use for the given request,
  2. // consulting r.Method, r.Host, and r.URL.Path. It always returns
  3. // a non-nil handler. If the path is not in its canonical form, the
  4. // handler will be an internally-generated handler that redirects
  5. // to the canonical path. If the host contains a port, it is ignored
  6. // when matching handlers.
  7. //
  8. // The path and host are used unchanged for CONNECT requests.
  9. //
  10. // Handler also returns the registered pattern that matches the
  11. // request or, in the case of internally-generated redirects,
  12. // the path that will match after following the redirect.
  13. //
  14. // If there is no registered handler that applies to the request,
  15. // Handler returns a “page not found” handler and an empty pattern.
  16. func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
  17.         if use121 {
  18.                 return mux.mux121.findHandler(r)
  19.         }
  20.         h, p, _, _ := mux.findHandler(r)
  21.         return h, p
  22. }
复制代码
总结

简单的回顾下,net/http通过ListenAndServe启动HTTP服务,监听指定的端口,当有请求到达时,启动一个goroutine来处理该请求(net/http/server.go),在完成HTTP的解析后,会调用Handler.ServeHTTP方法进行处理,该方法可以通过实现Handler接口来自定义,并在调用ListenAndServe时进行设置。
Engine: Gin的核心

上面已经说了,Gin是通过实现http.Handler接口实现的,在Gin中实现这个接口的就是Engine,所以要了解Gin的结构从Engine入手是比较方便的。
ServeHttp

net/http在接收到一个请求后,会创建一个goroutine处理该请求,在读取数据并进行解析后,会调用ServeHttp
  1. func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  2.         handler := sh.srv.Handler
  3.         if handler == nil {
  4.                 handler = DefaultServeMux
  5.         }
  6.         if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
  7.                 handler = globalOptionsHandler{}
  8.         }
  9.         handler.ServeHTTP(rw, req)
  10. }
复制代码
ServeHttp处理流程
graph TD         A[请求到来] --> B[从 sync.Pool 获取 Context]         B --> C[重置 Context 状态]         C --> D[处理 HTTP 请求]         D --> E[将 Context 归还 sync.Pool]         E --> F[响应返回]Engine的ServeHttp的实现为:
  1. // ServeHTTP conforms to the http.Handler interface.
  2. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  3.         c := engine.pool.Get().(*Context)
  4.         c.writermem.reset(w)
  5.         c.Request = req
  6.         c.reset()
  7.         engine.handleHTTPRequest(c)
  8.         engine.pool.Put(c)
  9. }
复制代码
在处理每个请求时,Gin都会为该请求分配一个gin.Context对象用来管理请求的上下文。为了避免频繁的创建和销毁gin.Context,使用了engine.pool来缓存gin.Context对象,每个请求到达时,先从pool中获取一个gin.Context对象,并重置gin.Context的状态,之后将gin.Context传递下去,请求处理完成后再将gin.Context返回对象池中。
在获取到gin.Context之后,通过HandleHttpRequest处理请求,HandleHttpRequest先根据URL确定路由,并获取绑定到路由路径上的所有handle,也就是middleWare,这些middleWare组成一个处理链,之后依次执行链上的handle
sync.pool 解析

engine.pool的类型是sync.Pool,是一个线程安全的对象池,提供了Get()和Put()方法,可以在多个goroutine中同时使用。
其内部设计优先考虑本地goroutine缓存以减少锁竞争(每个goroutine都有一个私有的本地缓存),当 Get() 时会首先从本地缓存获取,本地没有再从共享池中获取,Put() 时也优先放回本地缓存。
为什么要重置gin.Context?
sync.Pool不适合存放有状态且不能被重置的对象。gin.Context就是一个典型的例子,它会存储请求相关的状态,例如Request、ResponseWriter以及在中间件中传递的Keys。如果不重置,下一个请求可能会使用到上一个请求的残留数据,导致逻辑错误。
Gin通过在ServeHTTP方法中调用c.reset()来解决这个问题。reset方法会将Context的状态(如Request、ResponseWriter、Keys、index等)恢复到初始状态,确保每个请求都能获得一个干净的上下文。
路由和中间件

Gin的核心是Engine和RouterGroup,实际上,Engine嵌入了RouterGroup,也是一个RouterGroup
  1. // Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
  2. // Create an instance of Engine, by using New() or Default()
  3. type Engine struct {
  4.         RouterGroup
  5.         ...
  6. }
复制代码
Gin中的路由通过前缀树(Radix Tree)进行保存, Engine就是根RouterGroup
  1. // RouterGroup is used internally to configure router, a RouterGroup is associated with
  2. // a prefix and an array of handlers (middleware).
  3. type RouterGroup struct {
  4.         Handlers HandlersChain
  5.         basePath string
  6.         engine   *Engine
  7.         root     bool
  8. }
复制代码
Handlers 存储了该层级注册的中间件
basePath 用于管理路由组的前缀路径,方便路由组织
Gin中的路由通过前缀树(Radix Tree)进行保存,这种数据结构能够高效地进行动态路由匹配。
graph TD         A[Engine] --> B[RouterGroup /api/v1]         A --> C[RouterGroup /admin]         B --> D[GET /api/v1/users]         B --> E[POST /api/v1/login]         C --> F[GET /admin/dashboard]         C --> G[POST /admin/settings]当你在 GET, POST 等方法中注册一个路由时,Gin 会执行以下步骤来生成完整的处理链:

  • 获取父级中间件:首先,它会从当前的 RouterGroup(或 Engine)中获取其 Handlers 切片。
  • 拼接处理函数:然后,它会将本次注册的路由处理函数(handler...)追加到这个切片的末尾。
  • 存储到路由树:最终,这个完整的处理链会作为一个整体,与请求方法和路由路径一起,存储到 Gin 的路由树(一个前缀树 Radix Tree)中。
路由机制的优势

使用Radix Tree作为路由匹配的核心,带来了以下好处:

  • 高效匹配:能快速定位到匹配的路由,尤其是在路由数量庞大时。
  • 支持动态路由:可以轻松处理/users/:id这类带有参数的路由。
  • 支持通配符:可以处理/static/*filepath这类通配符路由。
Context

gin.Context贯穿整个HTTP请求生命周期,上下文对象除了保存数据用于在不同的中间件之间传递数据外,还提供了许多方法方便解析数据和响应数据,以及提供Next()和Abort()来控制流程
传递数据

gin.Context通过一个map[string]any对象来保存数据,并提供了Set和Get方法存取其中的数据
  1. type Context struct {
  2.         ...
  3.         // Keys is a key/value pair exclusively for the context of the request.
  4.         // New objects are not accepted.
  5.         Keys map[string]any
  6.         ...
  7. }
复制代码
封装当前请求和响应
  1.         c.writermem.reset(w)
  2.         c.Request = req
复制代码
上面ServeHTTP的实现中可以看到,会将net/http传递过来的http.ResponseWriter和*http.Request 保存到gin.Context中。
gin.Context提供和许多方法方便获取请求数据和返回响应,而不用直接操作http.ResponseWriter和*http.Request。
在读取数据时,如使用c.ShouldBindJSON(data)获取数据时,其实现就需要用到*http.Request解析数据:
  1. // Binding describes the interface which needs to be implemented for binding the
  2. // data present in the request such as JSON request body, query parameters or
  3. // the form POST.
  4. type Binding interface {
  5.         Name() string
  6.         Bind(*http.Request, any) error
  7. }
  8. // --- binding/json.go
  9. func (jsonBinding) Bind(req *http.Request, obj any) error {
  10.         if req == nil || req.Body == nil {
  11.                 return errors.New("invalid request")
  12.         }
  13.         return decodeJSON(req.Body, obj)
  14. }
复制代码
要返回JSON格式的响应数据,则可以调用c.JSON()将对象格式化为JSON格式。
Gin使用Render来格式化数据,Render的接口定义为:
  1. // Render interface is to be implemented by JSON, XML, HTML, YAML and so on.
  2. type Render interface {
  3.         // Render writes data with custom ContentType.
  4.         Render(http.ResponseWriter) error
  5.         // WriteContentType writes custom ContentType.
  6.         WriteContentType(w http.ResponseWriter)
  7. }
复制代码
其中的Render方法负责将数据格式化并写到http.ResponseWriter中
JSON的Render:
  1. // Render (JSON) writes data with custom ContentType.
  2. func (r JSON) Render(w http.ResponseWriter) error {
  3.         return WriteJSON(w, r.Data)
  4. }
  5. // WriteJSON marshals the given interface object and writes it with custom ContentType.
  6. func WriteJSON(w http.ResponseWriter, obj any) error {
  7.         writeContentType(w, jsonContentType)
  8.         jsonBytes, err := json.Marshal(obj)
  9.         if err != nil {
  10.                 return err
  11.         }
  12.         _, err = w.Write(jsonBytes)
  13.         return err
  14. }
复制代码
流程控制

请求到达后,会根据url进行路由,将匹配到的路由节点中的handlers的函数处理链保存到Context中的handlers属性中:
  1. type Context struct {
  2. ...
  3.         handlers HandlersChain
  4.         index    int8
  5. ...
  6. }
  7. // HandlersChain defines a HandlerFunc slice.
  8. type HandlersChain []HandlerFunc
复制代码
Next()方法实际上只是沿着函数处理链往下走:
  1. // Next should be used only inside middleware.
  2. // It executes the pending handlers in the chain inside the calling handler.
  3. // See example in GitHub.
  4. func (c *Context) Next() {
  5.         c.index++
  6.         for c.index < int8(len(c.handlers)) {
  7.                 c.handlers[c.index](c)
  8.                 c.index++
  9.         }
  10. }
复制代码
中间件处理流程示意图:
graph TD         A[请求] --> B[Middleware A]         B --> C{调用 c.Next}        C --> D[Middleware B]         D --> E[调用 c.Next]        E --> F[业务处理函数]         F --> G[Middleware B]         G --> H[Middleware A]         H --> I[响应]                 subgraph "中间件执行顺序"                 direction LR                 B --> D --> F         end                 subgraph "响应返回顺序"                 direction LR                 F --> G --> H         endAbort()将index设置为math.MaxInt8 >> 1来终止整个调用链:
  1. // abortIndex represents a typical value used in abort functions.
  2. const abortIndex int8 = math.MaxInt8 >> 1
  3. // Abort prevents pending handlers from being called. Note that this will not stop the current handler.
  4. // Let's say you have an authorization middleware that validates that the current request is authorized.
  5. // If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers
  6. // for this request are not called.
  7. func (c *Context) Abort() {
  8.         c.index = abortIndex
复制代码
总结

Gin框架通过以下核心机制实现了简洁高效的Web服务:

  • 基于net/http:利用其Handler接口和并发模型,解耦了网络细节和业务逻辑。
  • Engine和sync.Pool:通过Engine作为核心处理器,并使用对象池高效管理gin.Context对象。
  • Radix Tree路由:实现了高性能、支持动态路由的请求匹配。
  • Context对象:贯穿请求生命周期,封装了请求和响应,提供了流程控制和数据传递能力。
  • 中间件机制:通过HandlersChain和Next()、Abort()实现了灵活的请求处理管道。
这些设计共同构成了Gin简洁而强大的架构,使其成为Go语言Web开发中的热门选择。
仓库地址:lapluma
微信公众号:午夜游鱼
1.png


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册