之前探究了go原生net/http的启动过程,现在使用gin框架来启动一个后端接口。
func main() {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
router.Run()
}
在gin框架中,Engine是整个Web应用的核心,它集成了路由、中间件管理、HTTP 服务运行、模板渲染、错误处理等所有核心功能。所有请求的入口和出口都由它来调度。
相对于直接操作底层的net/http,Engine有以下的好处:
- 封装与简化:
Engine封装了net/http中复杂的ServeMux和Handler逻辑。它提供了更直观、更强大的路由定义方式(如路由分组、参数路由)和中间件机制,极大地提升了开发效率。 - 高性能路由:
gin的Engine内部使用了一个名为httprouter的定制化高性能路由库。它使用基数树(Radix Tree) 来存储和匹配路由,使得路由查找速度极快,且不受注册路由数量多少的影响。 - 中间件流水线:
Engine提供了统一的中间件注册和管理能力。中间件可以全局作用于所有路由,也可以作用于特定路由组,这种链式处理机制是构建现代 Web 应用(如认证、日志、跨域处理)的基石。
Engine的创建
gin.Default()的源码如下:
func Default(opts ...OptionFunc) *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine.With(opts...)
}
其通过gin内部的一个名为New的函数创建一个Engine对象,并使用Use方法增加两个中间件Logger()以及Recovery(),最后使用OptionFunc来修改engine的参数。
New()
New()函数的源码如下:
func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew()
//创建一个Engine
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
//设置路由的根路径
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
UseEscapedPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
//初始化路由树
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
//默认信任所有代理
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
engine.engine = engine
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine.With(opts...)
}
这份源码engine.engine = engine中engine.engine是Engine结构体中嵌入字段RouterGroup字段,这使得RouterGroup能够反过来访问到Engine。
为什么要这么做?
首先,gin.Engine结构体内嵌了gin.RouterGroup。这意味着Engine是一个RouterGroup,继承了其所有方法(如 GET, POST, Use等)。
其次,gin的核心路由树(engine.trees)是存放在 Engine层级,而不是 RouterGroup层级的。但POST这类添加处理逻辑的方法是在RouterGroup上定义的。
这就产生了一个问题:当一个 RouterGroup(包括顶层的 Engine自己,以及通过 Group()创建的子分组)调用 GET(“/path”, handler)时,这个方法最终需要去修改 Engine.trees。然而,一个普通的 RouterGroup实例并不知道它所属于的那个顶层的 Engine是谁。
通过engine.engine = engine就可以解决这个问题。
使用Use()增加中间件
Use()的源码如下:
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
本质上是给engine中的RouterGroup增加中间件,其源码如下:
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
使用OptionFunc修改Engine
先查看什么是OptionFunc:
type OptionFunc func(*Engine)
OptionFunc的本质是一个接收Engine的指针,对Engine进行修改的函数
在engine.With()中,源码如下:
func (engine *Engine) With(opts ...OptionFunc) *Engine {
for _, opt := range opts {
opt(engine)
}
return engine
}
即With()方法的作用是遍历这些OptionFunc,并利用这些OptionFunc来修改engine。
现在有一个问题:既然New()函数的最后也使用了With()方法,为什么不将Default()修改为以下的内容?
func Default(opts ...OptionFunc) *Engine {
debugPrintWARNINGDefault()
engine := New(opts...)
engine.Use(Logger(), Recovery())
return engine
}
原因是要保证用户自己的配置大于代码约定,比如用户写了一个中间件清空的OptionFunc
func main() {
clearMiddlerFunc := func(engine *gin.Engine) {
engine.Handlers = make(gin.HandlersChain, 0)
}
router := gin.Default(clearMiddlerFunc)
router.With(func(engine *gin.Engine) {
engine.RedirectTrailingSlash = false
})
router.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
router.Run()
}
如果是先清空中间件,再先配置Logger以及Recovery,这就与用户实际的需求不相符。
路径和处理逻辑的绑定
在这一部分中,我们讨论在router.GET()的内部发生了什么。
源码如下:
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
可以看出,它和其他的方法POST,PATCH等底层都是group.handle()。
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
在其中,首先计算请求的绝对路径,其等于group的根路径+传入的相对路径。
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
其次,获取该请求的逻辑处理链,其等于group中的HandlersChain+传入的逻辑处理链。
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
然后,将请求方法类型,请求的绝对路径,请求的逻辑处理链作为参数添加到Engine的路由,最后返回一个returnedObj()的返回值。
func (group *RouterGroup) returnObj() IRoutes {
if group.root {
return group.engine
}
return group
}
此处的逻辑是:如果当前组是根组,返回最上层的Engine,如果是子组,返回子组本身。
为什么要加以判断呢?
我的理解是,在根组的情况下,一般直接使用Engine,而不是Engine.RouterGroup,为此保证调用者和实际返回类型一致,才做如此判断。而子组本身就是一个RouterGroup,因此直接返回。
路由添加
在Engine中最重要的便是路由的添加,下面将深入这一块的代码:
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(path[0] == '/', "path must begin with '/'")
assert1(method != "", "HTTP method can not be empty")
assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers)
root := engine.trees.get(method)
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
engine.maxSections = sectionsCount
}
}
首要便是三个判断,要求:
- 路径要求以
/为起点 - 请求方法不能为空
- 逻辑处理链的长度大于0
然后根据请求方法取出对应树的根节点
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
engine.trees是一个[]methodTree,它存储了每一种请求方法的路由树
type methodTree struct {
method string
root *node
}
如果当前的请求方法没有对应的methodTree,则创建新的树根节点,并将这棵新树添加到 engine.trees 中。
再调用root.addRoute()将路径和逻辑处理链加入路由。
最后记录最大路径参数数量和最大路径段数,避免在每次HTTP请求处理中重复分配内存,实现零内存分配的高性能路由匹配。
Engine的运行
以下是Run()的源码:
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
engine.updateRouteTrees()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
server := &http.Server{ // #nosec G112
Addr: address,
Handler: engine.Handler(),
}
err = server.ListenAndServe()
return
}
首先会解析地址
- 如果没有传入地址,将获取名为
PORT的环境变量,如果该变量不存在,返回8080,如果存在,返回该变量。 - 如果传入地址数为1,返回该地址
- 如果传入地址数过多,报错
func resolveAddress(addr []string) string {
switch len(addr) {
case 0:
if port := os.Getenv("PORT"); port != "" {
debugPrint("Environment variable PORT="%s"", port)
return ":" + port
}
debugPrint("Environment variable PORT is undefined. Using port :8080 by default")
return ":8080"
case 1:
return addr[0]
default:
panic("too many parameters")
}
}
其次,创建一个http服务器,传入地址以及Handler,并运行这个服务器。
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
engine.updateRouteTrees()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
server := &http.Server{ // #nosec G112
Addr: address,
Handler: engine.Handler(),
}
err = server.ListenAndServe()
return
}
Handler的传入
Handler是net/http包中的一个接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
而传给http服务器的Handler是engine.Handler()的返回值
func (engine *Engine) Handler() http.Handler {
if !engine.UseH2C {
return engine
}
h2s := &http2.Server{}
return h2c.NewHandler(engine, h2s)
}
不论是什么情况,Engine都直接作为Hanlder被返回或者使用,因为Engine实现了Handler接口。
一次请求的过程
前面的过程与原生http都一样,直到这个调用
serverHandler{c.server}.ServeHTTP(w, w.req)
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)
}
Engine顶替了默认的DefaultServeMux,来处理请求。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.routeTreesUpdated.Do(func() {
engine.updateRouteTrees()
})
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
如果是第一次请求,将通过 sync.Once的 Do方法,将路由树中的字符替换。
然后从池中取出一个gin的Context,将http.ResponseWriter传入。
最后使用handleHTTPRequest处理请求:
- 先通过请求方法获取到对应的路由树
- 再通过路由树获取到对应的路由节点
- 如果对应的处理逻辑链存在,将其赋给
Context,并调用c.Next()
func (c *Context) Next() {
c.index++
for c.index < safeInt8(len(c.handlers)) {
if c.handlers[c.index] != nil {
c.handlers[c.index](c)
}
c.index++
}
}
在其中不断调用处理逻辑处理请求。