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接口- type Handler interface{
- ServeHTTP(ResponseWriter, *Request)
- }
复制代码 任何实现了这个接口的类型都可以作为一个HTTP请求处理器。ServeHTTP 方法接收一个 http.ResponseWriter 和一个 *http.Request,分别用于写入响应和读取请求信息。
这种设计将业务逻辑与底层的网络细节彻底解耦,开发者只需关注如何处理请求和生成响应即可。
流程说明
net/http可以通过调用ListenAndServe监听端口,启动HTTP服务
使用net/http启动HTTP服务的简单示例:- func main() {
- http.Handle("/foo", fooHandler)
- http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
- })
- log.Fatal(http.ListenAndServe(":8080", nil))
- }
复制代码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- // ListenAndServe listens on the TCP network address addr and then calls
- // [Serve] with handler to handle requests on incoming connections.
- // Accepted connections are configured to enable TCP keep-alives.
- //
- // The handler is typically nil, in which case [DefaultServeMux] is used.
- //
- // ListenAndServe always returns a non-nil error.
- func ListenAndServe(addr string, handler Handler) error {
- server := &Server{Addr: addr, Handler: handler}
- return server.ListenAndServe()
- }
复制代码 ListenAndServe的第二个参数接收的就是前面说的实现了http.Handler接口的类型,默认的DefaultServeMux也是实现了该接口的一个类型,Gin的Engine也是一个实现了http.Handler的类型
DefaultServeMux:- // Handler returns the handler to use for the given request,
- // consulting r.Method, r.Host, and r.URL.Path. It always returns
- // a non-nil handler. If the path is not in its canonical form, the
- // handler will be an internally-generated handler that redirects
- // to the canonical path. If the host contains a port, it is ignored
- // when matching handlers.
- //
- // The path and host are used unchanged for CONNECT requests.
- //
- // Handler also returns the registered pattern that matches the
- // request or, in the case of internally-generated redirects,
- // the path that will match after following the redirect.
- //
- // If there is no registered handler that applies to the request,
- // Handler returns a “page not found” handler and an empty pattern.
- func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
- if use121 {
- return mux.mux121.findHandler(r)
- }
- h, p, _, _ := mux.findHandler(r)
- return h, p
- }
复制代码 总结
简单的回顾下,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- func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
- handler := sh.srv.Handler
- if handler == nil {
- handler = DefaultServeMux
- }
- if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
- handler = globalOptionsHandler{}
- }
- handler.ServeHTTP(rw, req)
- }
复制代码 ServeHttp处理流程
graph TD A[请求到来] --> B[从 sync.Pool 获取 Context] B --> C[重置 Context 状态] C --> D[处理 HTTP 请求] D --> E[将 Context 归还 sync.Pool] E --> F[响应返回]Engine的ServeHttp的实现为:- // ServeHTTP conforms to the http.Handler interface.
- func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
- c := engine.pool.Get().(*Context)
- c.writermem.reset(w)
- c.Request = req
- c.reset()
- engine.handleHTTPRequest(c)
- engine.pool.Put(c)
- }
复制代码 在处理每个请求时,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- // Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
- // Create an instance of Engine, by using New() or Default()
- type Engine struct {
- RouterGroup
- ...
- }
复制代码 Gin中的路由通过前缀树(Radix Tree)进行保存, Engine就是根RouterGroup- // RouterGroup is used internally to configure router, a RouterGroup is associated with
- // a prefix and an array of handlers (middleware).
- type RouterGroup struct {
- Handlers HandlersChain
- basePath string
- engine *Engine
- root bool
- }
复制代码 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方法存取其中的数据- type Context struct {
- ...
- // Keys is a key/value pair exclusively for the context of the request.
- // New objects are not accepted.
- Keys map[string]any
- ...
- }
复制代码 封装当前请求和响应
- c.writermem.reset(w)
- c.Request = req
复制代码 上面ServeHTTP的实现中可以看到,会将net/http传递过来的http.ResponseWriter和*http.Request 保存到gin.Context中。
gin.Context提供和许多方法方便获取请求数据和返回响应,而不用直接操作http.ResponseWriter和*http.Request。
在读取数据时,如使用c.ShouldBindJSON(data)获取数据时,其实现就需要用到*http.Request解析数据:- // Binding describes the interface which needs to be implemented for binding the
- // data present in the request such as JSON request body, query parameters or
- // the form POST.
- type Binding interface {
- Name() string
- Bind(*http.Request, any) error
- }
- // --- binding/json.go
- func (jsonBinding) Bind(req *http.Request, obj any) error {
- if req == nil || req.Body == nil {
- return errors.New("invalid request")
- }
- return decodeJSON(req.Body, obj)
- }
复制代码 要返回JSON格式的响应数据,则可以调用c.JSON()将对象格式化为JSON格式。
Gin使用Render来格式化数据,Render的接口定义为:- // Render interface is to be implemented by JSON, XML, HTML, YAML and so on.
- type Render interface {
- // Render writes data with custom ContentType.
- Render(http.ResponseWriter) error
- // WriteContentType writes custom ContentType.
- WriteContentType(w http.ResponseWriter)
- }
复制代码 其中的Render方法负责将数据格式化并写到http.ResponseWriter中
JSON的Render:- // Render (JSON) writes data with custom ContentType.
- func (r JSON) Render(w http.ResponseWriter) error {
- return WriteJSON(w, r.Data)
- }
- // WriteJSON marshals the given interface object and writes it with custom ContentType.
- func WriteJSON(w http.ResponseWriter, obj any) error {
- writeContentType(w, jsonContentType)
- jsonBytes, err := json.Marshal(obj)
- if err != nil {
- return err
- }
- _, err = w.Write(jsonBytes)
- return err
- }
复制代码 流程控制
请求到达后,会根据url进行路由,将匹配到的路由节点中的handlers的函数处理链保存到Context中的handlers属性中:- type Context struct {
- ...
- handlers HandlersChain
- index int8
- ...
- }
- // HandlersChain defines a HandlerFunc slice.
- type HandlersChain []HandlerFunc
复制代码 Next()方法实际上只是沿着函数处理链往下走:- // Next should be used only inside middleware.
- // It executes the pending handlers in the chain inside the calling handler.
- // See example in GitHub.
- func (c *Context) Next() {
- c.index++
- for c.index < int8(len(c.handlers)) {
- c.handlers[c.index](c)
- c.index++
- }
- }
复制代码 中间件处理流程示意图:
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来终止整个调用链:- // abortIndex represents a typical value used in abort functions.
- const abortIndex int8 = math.MaxInt8 >> 1
- // Abort prevents pending handlers from being called. Note that this will not stop the current handler.
- // Let's say you have an authorization middleware that validates that the current request is authorized.
- // If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers
- // for this request are not called.
- func (c *Context) Abort() {
- c.index = abortIndex
复制代码 总结
Gin框架通过以下核心机制实现了简洁高效的Web服务:
- 基于net/http:利用其Handler接口和并发模型,解耦了网络细节和业务逻辑。
- Engine和sync.Pool:通过Engine作为核心处理器,并使用对象池高效管理gin.Context对象。
- Radix Tree路由:实现了高性能、支持动态路由的请求匹配。
- Context对象:贯穿请求生命周期,封装了请求和响应,提供了流程控制和数据传递能力。
- 中间件机制:通过HandlersChain和Next()、Abort()实现了灵活的请求处理管道。
这些设计共同构成了Gin简洁而强大的架构,使其成为Go语言Web开发中的热门选择。
仓库地址:lapluma
微信公众号:午夜游鱼
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |