Gin 框架核心架构解析

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

net/http库概览

Gin是基于net/http实现的, 在介绍Gin之前,不妨先了解下net/http

net/http提供了HTTP客户端和服务端的功能,这里主要关心服务端的功能

服务端核心设计

得益于Go的并发模型,这里并不需要关心像传统处理网络请求的ReactorProactor的I/O复用机制,而是为每个请求都创建一个独立的goroutine来处理(对这部分有兴趣可以了解下Go的调度器和goroutine)。

net/http服务端功能的核心设计是http.Hander接口

go 复制代码
type Handler interface{
	ServeHTTP(ResponseWriter, *Request)
}

任何实现了这个接口的类型都可以作为一个HTTP请求处理器。ServeHTTP 方法接收一个 http.ResponseWriter 和一个 *http.Request,分别用于写入响应和读取请求信息。

这种设计将业务逻辑与底层的网络细节彻底解耦,开发者只需关注如何处理请求和生成响应即可。

流程说明

net/http可以通过调用ListenAndServe监听端口,启动HTTP服务

使用net/http启动HTTP服务的简单示例:

go 复制代码
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

go 复制代码
// 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

go 复制代码
// 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

go 复制代码
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[响应返回]

EngineServeHttp的实现为:

go 复制代码
// 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就是一个典型的例子,它会存储请求相关的状态,例如RequestResponseWriter以及在中间件中传递的Keys。如果不重置,下一个请求可能会使用到上一个请求的残留数据,导致逻辑错误。

Gin通过在ServeHTTP方法中调用c.reset()来解决这个问题。reset方法会将Context的状态(如RequestResponseWriterKeysindex等)恢复到初始状态,确保每个请求都能获得一个干净的上下文。

路由 和中间件

Gin的核心是EngineRouterGroup,实际上,Engine嵌入了RouterGroup,也是一个RouterGroup

go 复制代码
// 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

go 复制代码
// 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 会执行以下步骤来生成完整的处理链:

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

路由机制的优势

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

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

Context

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

传递数据

gin.Context通过一个map[string]any对象来保存数据,并提供了SetGet方法存取其中的数据

go 复制代码
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
	...
}

封装当前请求和响应

go 复制代码
	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解析数据:

go 复制代码
// 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的接口定义为:

go 复制代码
// 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:

go 复制代码
// 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属性中:

go 复制代码
type Context struct {
...
	handlers HandlersChain
	index    int8
...
}

// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc

Next()方法实际上只是沿着函数处理链往下走:

go 复制代码
// 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 end

Abort()index设置为math.MaxInt8 >> 1来终止整个调用链:

go 复制代码
// 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接口和并发模型,解耦了网络细节和业务逻辑。
  • Enginesync.Pool :通过Engine作为核心处理器,并使用对象池高效管理gin.Context对象。
  • Radix Tree路由:实现了高性能、支持动态路由的请求匹配。
  • Context对象:贯穿请求生命周期,封装了请求和响应,提供了流程控制和数据传递能力。
  • 中间件机制 :通过HandlersChainNext()Abort()实现了灵活的请求处理管道。

这些设计共同构成了Gin简洁而强大的架构,使其成为Go语言Web开发中的热门选择。

原文地址:muzhy.github.io/posts/gin_a...

相关推荐
寻月隐君6 分钟前
Rust 实战:从零构建一个多线程 Web 服务器
后端·rust·github
Livingbody1 小时前
FastMCP In Action之 Server详解
后端
GetcharZp2 小时前
C++ Boost 从入门到精通:让你的代码飞起来
c++·后端
北'辰2 小时前
DeepSeek智能考试系统智能体
前端·后端·架构·开源·github·deepseek
hrrrrb2 小时前
【Spring Boot 快速入门】八、登录认证(二)统一拦截
hive·spring boot·后端
_風箏4 小时前
OpenSSH【安装 03】远程代码执行漏洞CVE-2024-6387修复(cp: 无法创建普通文件“/usr/sbin/sshd“:文本文件忙问题处理)
后端
我是哪吒4 小时前
分布式微服务系统架构第164集:架构懂了就来了解数据库存储扩展千亿读写
后端·面试·github
UrbanJazzerati4 小时前
PowerShell 自动化实战:自动化为 Git Staged 内容添加 Issue 注释标记
后端·面试·shell
橙序员小站4 小时前
通过trae开发你的第一个Chrome扩展插件
前端·javascript·后端