Gin使用及源码简析
1. Gin简介
前面通过两篇文章分享了Golang HTTP编程的路由分发、请求/响应处理。
可以看出来Golang原生HTTP编程在路由分组、动态路由及参数读取/验证、构造String/Data/JSON/HTML响应的方法等存在优化的空间。
Gin是一个用Golang编写的高性能Web框架。
- 基于前缀树的路由,快速且支持动态路由
- 支持中间件及路由分组,将具有同一特性的路由划入统一组别、设置相同的中间件。
- 比如需要登录的一批接口接入登录权限认证中间件、而不需要登录一批接口则不需要接入
- ...
2. 快速使用
基于gin@v1.8.1
,基本使用如下
func main() { // Creates a new blank Engine instance without any middleware attached engine := gin.New() // Global middleware // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release. // By default gin.DefaultWriter = os.Stdout engine.Use(gin.Logger()) // Recovery middleware recovers from any panics and writes a 500 if there was one. engine.Use(gin.Recovery()) v1Group := engine.Group("app/v1", accessHandler()) v1Group.GET("user/info", userInfoLogic()) engine.Run(":8019") }
终端运行go run main.go
,输出如下
$ go run main.go [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /app/v1/user/info --> main.userInfoLogic.func1 (4 handlers) [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value. Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details. [GIN-debug] Listening and serving HTTP on :8019
通过打印可以看出注册了GET方法的路由/app/v1/user/info
,对应处理函数为main.userInfoLogic
,
总共包括四个处理函数,按顺序为gin.Logger()
、gin.Recovery()
、accessHandler()
以及userInfoLogic
最终在端口8019启动了HTTP监听服务。
2.1 创建Engine
并使用gin.Logger()
和 gin.Recovery()
两个全局中间件,对engine
下的所有路由都生效
通过代码及注释,gin.Logger()
和 gin.Recovery()
放到了Engine.RouterGroup.Handlers
切片中。
// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be // included in the handlers chain for every single request. Even 404, 405, static files... // For example, this is the right place for a logger or error management middleware. func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { engine.RouterGroup.Use(middleware...) engine.rebuild404Handlers() engine.rebuild405Handlers() return engine } // Use adds middleware to the group, see example code in GitHub. func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() }
2.2 创建路由分组v1Group
,且该分组使用了accessHandler()
,accessHandler()
对v1Group
分组路由均生效
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix. // For example, all the routes that use a common middleware for authorization could be grouped. func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { return &RouterGroup{ Handlers: group.combineHandlers(handlers), basePath: group.calculateAbsolutePath(relativePath), engine: group.engine, } }
从代码可以看出,返回了新的gin.RouterGroup
,并且
v1Group.Handlers = append(group.Handlers, handlers)
,此时gin.RouterGroup.Handlers
为[gin.Logger(),gin.Recovery(),accessHandler()]
同时v1Group.basePath = "app/v1"
从代码同时可以得出,支持分组嵌套分组。即在v1Group
都基础上在创建分组,比如v1Group.Group("north")
2.3 在v1Group
下注册路由user/info
,该路由的处理函数是userInfoLogic
,方法为GET
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() } func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { ... 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) ... }
将分组v1Group
的路由前缀和当前user/info
计算得到完整路由,即app/v1/user/info
合并处理函数,此时handlers = [gin.Logger(),gin.Recovery(),accessHandler(),userInfoLogic()]
最后将路由及处理函数按http method分组,加入到不同路由树中。
2.4 通过 engine.Run(":8019")
在启动HTTP服务
// Run attaches the router to a http.Server and starts listening and serving HTTP requests. // It is a shortcut for http.ListenAndServe(addr, router) // Note: this method will block the calling goroutine indefinitely unless an error happens. func (engine *Engine) Run(addr ...string) (err error) { ... address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine.Handler()) return }
这里调用http.ListenAndServe
启动HTTP
监听服务,Engine
实现了http.Handler
接口,如果有客户端请求,会调用到Engine.ServeHTTP
函数。
3. 路由过程
// gin.go func (engine *Engine) handleHTTPRequest(c *Context) { httpMethod := c.Request.Method rPath := c.Request.URL.Path ... // Find root of the tree for the given HTTP method t := engine.trees for i, tl := 0, len(t); i < tl; i++ { if t[i].method != httpMethod { continue } root := t[i].root // Find route in tree value := root.getValue(rPath, c.params, c.skippedNodes, unescape) if value.params != nil { c.Params = *value.params } if value.handlers != nil { c.handlers = value.handlers c.fullPath = value.fullPath c.Next() c.writermem.WriteHeaderNow() return } ... break } } // context.go func (c *Context) Next() { c.index++ for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } }
从上面代码可以看出,通过http method找到对应的路由树,再根据URL从路由树中查找对应的节点,
获取到处理函数切片,通过c.Next
按通过顺序执行处理函数。
对于请求GET /app/v1/user/info,将依次执行[gin.Logger(),gin.Recovery(),accessHandler(),userInfoLogic()]
4. 请求/响应参数处理
func accessHandler() func(*gin.Context) { return func(c *gin.Context) { // 不允许crul访问 if strings.Contains(c.GetHeader("user-agent"), "curl") { c.JSON(http.StatusBadRequest, "cant't not visited by curl") c.Abort() // 直接退出,避免执行后续处理函数 } } } func userInfoLogic() func(*gin.Context) { return func(c *gin.Context) { id := c.Query("id") c.JSON(http.StatusOK, map[string]interface{}{"id": id, "name": "bob", "age": 18}) } }
v1Group
的通用处理函数accessHandler
,达到v1Group
下注册的路由无法用curl访问的效果。
通过c.Query("id")
获取URL查询参数,
通过以下代码可以看出,第一次获取URL查询时会缓存所有URL查询参数,这减少了内存的分配,节省了计算资源。
因为每次调用url.ParseQuery
都会重新申请缓存,重复解析URL。
func (c *Context) Query(key string) (value string) { value, _ = c.GetQuery(key) return } func (c *Context) initQueryCache() { if c.queryCache == nil { if c.Request != nil { c.queryCache = c.Request.URL.Query() } else { c.queryCache = url.Values{} } } } func (c *Context) GetQueryArray(key string) (values []string, ok bool) { c.initQueryCache() values, ok = c.queryCache[key] return }
通过c.JSON
返回Content-Type
为application/json
的响应体,
这也是Gin
对原生net/http编程的一个优化,对常用的响应类型进行封装,方便使用者使用。
当然,Gin对请求/响应参数的处理还有其它很多细微的优化,这里就不详细说明了。
5. 总结
Gin使用Map来实现路由匹配,而Gin使用路由树来实现路由匹配,支持动态路由,内存占用小且路由匹配快。
同时Gin使用缓存来优化请求参数的处理过程,提供了通用的响应参数处理等,方便用户使用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?