1. 从简单例子分析底层原理
官方提供了一个quick start 例子,我们在此基础上增加使用路由分组和中间件,后续一行一行分析gin核心源码
package mainimport ("github.com/gin-gonic/gin""net/http")func main() {g := gin.Default()r := g.Group("/my")r.Use(helloMiddelware(), byeMiddleware())r.GET("/ping", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"message": "pong",})})g.Run()}func helloMiddleware() gin.HandlerFunc {return func(c *gin.Context) {fmt.Println("hello...")c.Next()}}func byeMiddleware() gin.HandlerFunc {return func(c *gin.Context) {fmt.Println("bye...")c.Abort()}}
2. Engine实例的创建
我们先看第一行 g := gin.Default(),进入源码,可以看到是通过New()方法来创建Engine实例的。
func Default() *Engine {debugPrintWARNINGDefault()engine := New()engine.Use(Logger(), Recovery())return engine}
我们重点看New()方法
unc New() *Engine {// debug模式下的打印,可以忽略不看debugPrintWARNINGNew()engine := &Engine{// 顶端分组路由RouterGroup: RouterGroup{Handlers: nil,basePath: "/",root: true,},// 省略部分字段...// 最大多媒体内存MaxMultipartMemory: defaultMultipartMemory,// 初始化一个容量为9的方法前缀树切片,对应9种请求方法trees: make(methodTrees, 0, 9),// 模板渲染分隔符delims: render.Delims{Left: "{{", Right: "}}"},// 省略部分字段...}engine.RouterGroup.engine = engine// 为Engine的sync.Pool对象池指定生成对象的函数engine.pool.New = func() interface{} {return engine.allocateContext()}return engine}
从源码中我们可以看到New()方法主要做以下事情:
- 为Engine实例初始化RouterGroup
- 为Engine实例初始化MaxMultipartMemory
- 为Engine实例初始化一个容量为9的方法前缀树切片trees
- 为Engine实例初始化delims
- 为Engine的sync.Pool对象池指定生成对象的函数
其中engine.allocateContext()主要是初始化Context实例:
func (engine *Engine) allocateContext() *Context {v := make(Params, 0, engine.maxParams)skippedNodes := make([]skippedNode, 0, engine.maxSections)return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}}
3. 路由分组的创建
Engine实例创建完成后,我们来看第二行代码r := g.Group("/my")
// 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{// 合并HandlerFuncHandlers: group.combineHandlers(handlers),// 根据相对路径拼接出路由分组的完整路径basePath: group.calculateAbsolutePath(relativePath),// 把原路由的Engine传递给新路由engine: group.engine,}}
Group()方法主要做了以下事情:
- 原路由的Handlers和新加入的Handlers合并
- 根据相对路径拼接出新路由分组的完整路径
- 把原路由的Engine传递给新路由
我们可以稍微看看combineHandlers方法和calculateAbsolutePath:
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {// 新Handlers的总长度finalSize := len(group.Handlers) + len(handlers)// 长度大于等于63,则panicif finalSize >= int(abortIndex) {panic("too many handlers")}// 创建长度为finalSize的HandlerFunc切片(HandlersChain其实是[]HandlerFunc的别名)mergedHandlers := make(HandlersChain, finalSize)// 把原路由的HandlerFunc和新加入的HandlerFunc拷贝到新切片copy(mergedHandlers, group.Handlers)copy(mergedHandlers[len(group.Handlers):], handlers)return mergedHandlers}
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {// 根据原路由的路径和新的路径进行拼接return joinPaths(group.basePath, relativePath)}
func joinPaths(absolutePath, relativePath string) string {if relativePath == "" {return absolutePath}finalPath := path.Join(absolutePath, relativePath)if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {return finalPath + "/"}return finalPath}
4. 中间件
路由分组创建完成,我们可以使用Use方法来注册中间件:
// Use adds middleware to the group, see example code in GitHub.func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {// 把中间件追加到Handlers切片中group.Handlers = append(group.Handlers, middleware...)return group.returnObj()}
实际上Use方法只做了一件事情,就是把中间件追加到Handlers切片。但值得注意的是,在helloMiddleware中间件中我们使用了c.Next()来使得处理链继续流转:
// 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++// 索引小于该树节点的Handlers就继续遍历for c.index < int8(len(c.handlers)) {// 执行HandlerFuncc.handlers[c.index](c)// 流转到下一个HandlerFuncc.index++}}
其实从源码可以分析出,就算中间件不使用Next方法,处理链也是会往下流转的。但是问题来了,多次调用Next方法不就会执行多个遍历吗?
由于是HandlerFunc是公用同一个Context的,A遍历中执行HandlerFunc导致B遍历执行,假设现在只有两个中间件,在B遍历结束时c.index是大于 handlers长度的,现在回到了A,A执行代码c.index++必定大于handlers长度,所以A遍历不会往下执行,c.handlers也就只会被遍历一次。c.Abort()用于终止处理链:
func (c *Context) Abort() {c.index = abortIndex}
在路由分组创建中我们知道handlers最大长度是62,所以通过设置c.index=63即可终止handlers的遍历
5. 注册路由
从源码看,其实GET, POST, PUT, DELETE等方法都是通过group.handle处理:
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {// 拼接路径absolutePath := group.calculateAbsolutePath(relativePath)// 追加Handlershandlers = group.combineHandlers(handlers)// 把路由添加到路由树group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj()}
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {// 路径,方法,Handlers的校验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)// 省略部分代码...}
engine.addRoute主要做两件事:
- 获取方法树的根节点
- 在方法树中新增节点
6. 处理请求
gin运行服务和处理请求其实都是使用net/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) {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.")}// 处理IP地址address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)// 通过net/http的方法来运行socket服务err = http.ListenAndServe(address, engine)return}
我们知道,ListenAndServe方法的第二个参数是Handler接口,也就意味着Engine实现了该接口
// ServeHTTP conforms to the http.Handler interface.func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {// 从对象池中获取一个Context对象c := engine.pool.Get().(*Context)// 初始化Context的相关字段c.writermem.reset(w)c.Request = reqc.reset()// 处理请求engine.handleHTTPRequest(c)// 回收对象engine.pool.Put(c)}
这里真正处理请求的是handleHTTPRequest方法
func (engine *Engine) handleHTTPRequest(c *Context) {// 省略部分代码...// Find root of the tree for the given HTTP methodt := engine.trees// 查找该请求对应的方法树for i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root// 在方法树中查找路由// Find route in treevalue := root.getValue(rPath, c.params, c.skippedNodes, unescape)if value.params != nil {c.Params = *value.params}if value.handlers != nil {// 把路由的handlers和完整路径赋值给当前上下文c.handlers = value.handlersc.fullPath = value.fullPath// 遍历handlers处理请求c.Next()// 写响应头部c.writermem.WriteHeaderNow()return}if httpMethod != "CONNECT" && rPath != "/" {if value.tsr && engine.RedirectTrailingSlash {redirectTrailingSlash(c)return}if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {return}}break}// 如果不允许该请求方法处理,则在其他方法树中查找路由进行处理if engine.HandleMethodNotAllowed {for _, tree := range engine.trees {if tree.method == httpMethod {continue}if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {c.handlers = engine.allNoMethodserveError(c, http.StatusMethodNotAllowed, default405Body)return}}}// 没有在树中找到路由,则返回404c.handlers = engine.allNoRouteserveError(c, http.StatusNotFound, default404Body)}
7. 上下文
gin中的Context虽然实现了golang的context.Context接口,但是只有Value方法做了实现,它更多的是用来承载请求的字段和相应的处理方法,例如请求参数的获取和绑定
// Deadline always returns that there is no deadline (ok==false),// maybe you want to use Request.Context().Deadline() instead.func (c *Context) Deadline() (deadline time.Time, ok bool) {return}// Done always returns nil (chan which will wait forever),// if you want to abort your work when the connection was closed// you should use Request.Context().Done() instead.func (c *Context) Done() <-chan struct{} {return nil}// Err always returns nil, maybe you want to use Request.Context().Err() instead.func (c *Context) Err() error {return nil}// Value returns the value associated with this context for key, or nil// if no value is associated with key. Successive calls to Value with// the same key returns the same result.func (c *Context) Value(key interface{}) interface{} {if key == 0 {return c.Request}if keyAsString, ok := key.(string); ok {val, _ := c.Get(keyAsString)return val}return nil}
需要注意的是Copy方法,gin不建议在协程中使用Context,如果必须要使用也是使用副本。为什么呢?我们之前也分析到Context对象是从pool中获取的,当请求结束会回收Context,如果此时在另外的协程中使用Context就会导致panic。
