Gin框架源码解析

396 阅读7分钟

本文将从Gin框架的基本使用开始,由浅到深对源码进行解读。

1 基本使用

使用Gin框架创建一个http服务的基本流程:

  1. 创建一个引擎
  2. 使用中间件(可选)
  3. 创建组(可选)
  4. 注册服务
  5. 启动

后文将对这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()

总共三步:

  1. 计算当前绝对路径
  2. 取出「group」的handlers并和「当前方法」的handlers合并
  3. 在「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)
}