本文将从Gin框架的基本使用开始,由浅到深对源码进行解读。
1 基本使用
使用Gin框架创建一个http服务的基本流程:
- 创建一个引擎
- 使用中间件(可选)
- 创建组(可选)
- 注册服务
- 启动
后文将对这5个函数进行源码解读。
// 示例代码
func main() {
r := gin.Default() // 默认引擎
group := r.Group("group") // 注册组
group.Use(Authentication) // 使用中间件
{
group.GET("/hello", HelloHandler) // 注册服务
}
// 启动
if err := r.Run("localhost:8080"); err != nil {
fmt.Println("Run error:", err)
}
}
2 源码解读
接下来将从源代码的角度,由浅到深,对上文的每个函数进行解读。
2.1 gin.Default()
从源码中可以看到,「Default引擎」其实就是一个「初始引擎」加上了Logger和Recovery中间件。
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default(opts ...OptionFunc) *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine.With(opts...)
}
2.1.1 gin.New()
其实就是初始化engine结构体,为里面的字段附上默认值。
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, // 启用从客户端获取 IP 的配置
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, // 获取客户端 IP 的 HTTP 头部列表
TrustedPlatform: defaultPlatform, // 信任的平台来源,使用默认值
UseRawPath: false, // 禁用原始路径,自动解码 URL 编码字符
RemoveExtraSlash: false, // 不删除 URL 中的多余斜杠
UnescapePathValues: true, // 自动解码路径值中的转义字符
MaxMultipartMemory: defaultMultipartMemory, // 上传文件的最大内存限制
trees: make(methodTrees, 0, 9), // 初始化方法树,容量为 9
delims: render.Delims{Left: "{{", Right: "}}"}, // 设置模板标签定界符
secureJSONPrefix: "while(1);", // 安全 JSON 前缀,用于防止 JSON 注入
trustedProxies: []string{"0.0.0.0/0", "::/0"}, // 允许所有代理的 IP 段
trustedCIDRs: defaultTrustedCIDRs, // 受信任的 IP 段(使用默认值)
}
engine.RouterGroup.engine = engine // 将 RouterGroup 中的 engine 指向当前 engine 实例
engine.pool.New = func() any { // 分配Context对象的函数
return engine.allocateContext(engine.maxParams)
}
return engine.With(opts...) // 调用 With 方法应用传入的配置选项
}
2.1.2 engine.Use()
让根RouterGroup注册上中间件,并且「rebuild」404、405错误时的处理函数。
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
这里大家可能会疑惑「rebuild」是干什么用的?先说结论,其作用是将「中间件」的handlerChain和「noRoute」或「noMethod」的handlerChain进行合并,并赋值给engine结构体的「allNoRoute」或「allNoMethod」。因为当路由失败时,会执行「allNoRoute」或「allNoMethod」处理函数链,其构成为「中间件」+「noRoute | noMethod」,所以当注「中间件」的时候,要同步更新「allNoRoute」或「allNoMethod」。源码:
func (engine *Engine) rebuild404Handlers() {
engine.allNoRoute = engine.combineHandlers(engine.noRoute)
}
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers") // 函数链上限为62
mergedHandlers := make(HandlersChain, finalSize)
// 这里两个copy其实就是将「中间件」和「noRoute」的handlerChain进行合并
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
2.1.3 engine.With()
很简单,就是将传入的「函数选项」按顺序执行,一般用作engine的自定义初始化。
func (engine *Engine) With(opts ...OptionFunc) *Engine {
for _, opt := range opts {
opt(engine)
}
return engine
}
2.2 Group()
创建一个「组」并进行初始化,具体包括:继承「父组」的handlers、计算当前的「组」路径、将engine字段指向「父组」的engine。
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers),
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
2.3 group.Use()
可以看到这里的Use()和上文的engine.Use()相比,缺少了两行「rebuild」语句。这是因为当「路由失败」的时候,gin框架只会走「engine」注册的中间件,不会走「其他组」注册的中间件,所以对于「非engine的组」来说,没必要更新「AllNoRoute」或者「AllNoMethod」。
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
2.4 group.Get()
总共三步:
- 计算当前绝对路径
- 取出「group」的handlers并和「当前方法」的handlers合并
- 在「engine」中添加路由
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
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.engine.addRoute()这个函数。
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)
// 获取http方法对应的「压缩前缀树」的root
root := engine.trees.get(method)
if root == nil {
// 如果没有,则新建methodTree
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
// 添加path及其对应的handlers
// 这里的addRoute和构建「压缩前缀树」有关本文不详细介绍,个人计划在「压缩前缀树专题」中进行阐述
root.addRoute(path, handlers)
/*
更新engine的maxParams和engine的maxSection
目的:优化内存分配
预分配内存:在路由匹配过程中,可能需要存储和处理路径中的参数。通过预先知道路径中可能的最大参数数,可以更高效地分配内存,减少动态内存分配的开销
减少内存碎片:预分配内存可以减少内存碎片,提高内存使用的效率
*/
if paramsCount := countParams(path); paramsCount > engine.maxParams {
engine.maxParams = paramsCount
}
if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
engine.maxSections = sectionsCount
}
}
2.5 engine.Run()
解析地址,并使用http包来进行监听和服务。
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://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies> for details.")
}
// 解析地址
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
// 使用http包来监听和服务
err = http.ListenAndServe(address, engine.Handler())
return
}
func resolveAddress(addr []string) string {
// 如果地址为空,则先从环境变量中获取port,如果获取不到,则赋予默认值8080
// 如果有多个地址,则panic
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")
}
}
func (engine *Engine) Handler() http.Handler {
// 如果不启用http2的ClearText(即不使用TLS的http2),则返回引擎(默认为http1.1)
if !engine.UseH2C {
return engine
}
// 如果启用,则返回http2 CleartText的handler
h2s := &http2.Server{}
return h2c.NewHandler(engine, h2s)
}
engine实现了http.Handler,也就是实现了其中的ServeHTTP方法,这里我们将具体分析engine是如何接收request,并进行handle的:
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Gin使用对象池(sync.Pool)来复用Context实例,避免频繁的内存分配,提高性能
c := engine.pool.Get().(*Context)
// 重置Context的writermem。writermem是一个包装过的http.ResponseWriter,用于缓存响应内容
c.writermem.reset(w)
// 让Context持有当前请求的详细信息
c.Request = req
// 重置Context,因为这里的Context是从对象池拿出来的,具有上一个请求的信息
c.reset()
// 处理请求,详细请看下文
engine.handleHTTPRequest(c)
// 归还Context对象,供后续请求使用
engine.pool.Put(c)
}
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
unescape := false
// 如果启用了UseRawPath且RawPath存在,rPath就使用RawPath
// RawPath是原始的路径字符串,通常用于处理路径中的特殊字符
// 例如在RawPath中「%20」不会被解码为「空格」
// unescape 控制是否对路径值进行解码
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
// 如果启用了RemoveExtraSlash,就通过cleanPath函数去除路径中的多余斜杠
// 例如「/foo//bar」会变成「/foo/bar」
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath)
}
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
// 遍历9个方法的路由树,找到对应的路由树
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 在树中根据路径,找到对应的node,并返回node的信息
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
// 设置路由的params和handlers
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
// 依次执行handlerChain(处理函数链)上的所有handler
c.Next()
// 写入响应头,如果handlerChain中没有写入,这里会写入
c.writermem.WriteHeaderNow()
return
}
// 处理重定向
if httpMethod != http.MethodConnect && rPath != "/" {
// 如果请求路径存在多余的斜杠,例如「/foo」和「/foo/」
// 且启用了RedirectTrailingSlash,则会进行重定向
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
// redirectFixedPath 处理路径固定的重定向
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
// 如果启用了「HandleMethodNotAllowed」,并且请求的方法不被允许,
// Gin会根据「RFC 7231」返回「405 Method Not Allowed」状态码,
// 并在响应头中添加Allow字段,列出「该路径」支持的HTTP方法。
if engine.HandleMethodNotAllowed {
// According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response
// containing a list of the target resource's currently supported methods.
allowed := make([]string, 0, len(t)-1)
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
allowed = append(allowed, tree.method)
}
}
if len(allowed) > 0 {
c.handlers = engine.allNoMethod
c.writermem.Header().Set("Allow", strings.Join(allowed, ", "))
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
// 如果找不到匹配的路由并且没有开启「HandleMethodNotAllowed」,
// 或者找不到匹配的路由并且开启了「HandleMethodNotAllowed」,但是其他方法的路由树中也都没有匹配的路由
// Gin会使用「allNoRoute」的handlerChain,返回「404 Not Found」错误。
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}