【Gin】源码阅读:框架实例化和路由

上一篇中安装并通过官方提供的defaultdemo大概了解了一下gin,现在开始深入阅读gin的源码吧!

官方demo

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

其实官方demo很短,就3行。

  1. 获取一个框架实体
  2. 注册请求回调
  3. 启动

慢慢看。先看第一个:gin.Default()

gin.Default()

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

default方法返回了一个默认的框架实体

debugPrintWARNINGDefault()

打印日志用的,跳过他

engine := New()

这里实例化了一个框架实体

// New returns a new blank Engine instance without any
// By default the configuration is:
// - RedirectTrailingSlash:  true
// - RedirectFixedPath:      false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP:    true
// - UseRawPath:             false
// - UnescapePathValues:     true
func New() *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		FuncMap:                template.FuncMap{},
		RedirectTrailingSlash:  true,
		RedirectFixedPath:      false,
		HandleMethodNotAllowed: false,
		ForwardedByClientIP:    true,
		AppEngine:              defaultAppEngine,
		UseRawPath:             false,
		UnescapePathValues:     true,
		MaxMultipartMemory:     defaultMultipartMemory,
		trees:                  make(methodTrees, 0, 9),
		delims:                 render.Delims{Left: "{{", Right: "}}"},
		secureJsonPrefix:       "while(1);",
	}
	engine.RouterGroup.engine = engine
	engine.pool.New = func() interface{} {
		return engine.allocateContext()
	}
	return engine
}

使用 new() 方法返回一个没有注入任何中间件的空白的引擎实例。首先创建了一个 Engine 指针,看看 Engine 长什么样子

Engine
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
	RouterGroup

	// Enables automatic redirection if the current route can't be matched but a
	// handler for the path with (without) the trailing slash exists.
	// For example if /foo/ is requested but a route only exists for /foo, the
	// client is redirected to /foo with http status code 301 for GET requests
	// and 307 for all other request methods.
	RedirectTrailingSlash bool

	// If enabled, the router tries to fix the current request path, if no
	// handle is registered for it.
	// First superfluous path elements like ../ or // are removed.
	// Afterwards the router does a case-insensitive lookup of the cleaned path.
	// If a handle can be found for this route, the router makes a redirection
	// to the corrected path with status code 301 for GET requests and 307 for
	// all other request methods.
	// For example /FOO and /..//Foo could be redirected to /foo.
	// RedirectTrailingSlash is independent of this option.
	RedirectFixedPath bool

	// If enabled, the router checks if another method is allowed for the
	// current route, if the current request can not be routed.
	// If this is the case, the request is answered with 'Method Not Allowed'
	// and HTTP status code 405.
	// If no other Method is allowed, the request is delegated to the NotFound
	// handler.
	HandleMethodNotAllowed bool
	ForwardedByClientIP    bool

	// #726 #755 If enabled, it will thrust some headers starting with
	// 'X-AppEngine...' for better integration with that PaaS.
	AppEngine bool

	// If enabled, the url.RawPath will be used to find parameters.
	UseRawPath bool

	// If true, the path value will be unescaped.
	// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
	// as url.Path gonna be used, which is already unescaped.
	UnescapePathValues bool

	// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
	// method call.
	MaxMultipartMemory int64

	delims           render.Delims
	secureJsonPrefix string
	HTMLRender       render.HTMLRender
	FuncMap          template.FuncMap
	allNoRoute       HandlersChain
	allNoMethod      HandlersChain
	noRoute          HandlersChain
	noMethod         HandlersChain
	pool             sync.Pool
	trees            methodTrees
}

Engine 是框架的一个实例,包含了 muxer 请求回调处理器,middleware 中间件和配置。使用 new() 或者 defalut() 创建一个 Engine

Engine中的成员如下:

  1. RouterGroup:路由分组
  2. RedirectTrailingSlash:用来重定向修复路由尾部的 / ,当一个路由如 /xxx/ 找不到匹配时,配置此选项为true后会自动重定向尝试 /xxx 是否可以匹配,其中对于 get 请求返回301,其他请求返回307
  3. RedirectFixedPath:路由自动修复重定向,用来修复尾部的 // 并可以将路由变为全小写格式
  4. HandleMethodNotAllowed:检查请求方式,设置为true后使用不允许的方式请求会自动尝试其他方式的回调,设置为false为常见的405状态
  5. ForwardedByClientIP:是否转发客户端IP
  6. AppEngine:开启后会在header中增加一条 X-AppEngine...
  7. UseRawPath:开启后会使用 url.RawPath 去寻找请求参数
  8. UnescapePathValues:配合 url.Path 使用的,不转义请求路径
  9. 默认情况下,UseRawPath = false,UnescapePathValues = true
  10. MaxMultipartMemory:限制 Multipart 类型的表单大小(用来限制上传文件大小的)
  11. delims:渲染html模板的分隔符
  12. secureJsonPrefix:似乎是在上下文中的 Context.SecureJSON 中使用的,先扔在这吧
  13. HTMLRender:HTML模板渲染器
  14. FuncMap:注册给渲染器的方法
  15. allNoRoute:404的默认响应
  16. allNoMethod:405的默认响应
  17. noRoute:用来设置allNoRoute回调的
  18. noMethod:用来设置allNoMethod回调的
  19. pool:用来存放上下文的变量池
  20. trees:这是一个methodTree切片,是用来管理路由注册的,methodTree中包含了:
  21. 请求的方法(get,put,post等)
  22. 请求的节点

上面的内容中,关于模板的渲染器 template 包还没有说过,后面再看吧。如果一个项目需要用后端去渲染页面的话,为什么不用php呢?

上面这么多成员,最重要的还是 RouterGroup 路由分组,在路由分组中我们应该可以看到路由匹配相关的内容,这也是一个web框架的灵魂之一。第二可以看看用来注册请求回调的HandlersChain长什么样。

首先看看 HandlersChain 吧,router 可能涉及到的内容应该会比请求回调多,先看简单的。

HandlersChain
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc

// Last returns the last handler in the chain. ie. the last handler is the main one.
func (c HandlersChain) Last() HandlerFunc {
	if length := len(c); length > 0 {
		return c[length-1]
	}
	return nil
}

可以看到,HandlersChain 是一个 HandlerFunc 切片,而 handlerFunc 就是用来做返回的函数主体。

其中,HandlersChain 还有一个 Last() 方法,用来返回最后一个 handlerFunc 源码中注释说到 最后一个 handlerFunc 是最重要的那个

HandlerFunc 是一个函数型数据,他需要一个 Context 参数,也就是上一篇说到的上下文,在上一篇也看过了,上下文主要是保存了请求和响应的实例以及请求的参数等本次请求的信息。

RouterGroup

现在再来看看gin框架的router是怎么做的吧!

// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
	IRoutes
	Group(string, ...HandlerFunc) *RouterGroup
}

// IRoutes defines all router handle interface.
type IRoutes interface {
	Use(...HandlerFunc) IRoutes

	Handle(string, string, ...HandlerFunc) IRoutes
	Any(string, ...HandlerFunc) IRoutes
	GET(string, ...HandlerFunc) IRoutes
	POST(string, ...HandlerFunc) IRoutes
	DELETE(string, ...HandlerFunc) IRoutes
	PATCH(string, ...HandlerFunc) IRoutes
	PUT(string, ...HandlerFunc) IRoutes
	OPTIONS(string, ...HandlerFunc) IRoutes
	HEAD(string, ...HandlerFunc) IRoutes

	StaticFile(string, string) IRoutes
	Static(string, string) IRoutes
	StaticFS(string, http.FileSystem) IRoutes
}

// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
	Handlers HandlersChain
	basePath string
	engine   *Engine
	root     bool
}

var _ IRouter = &RouterGroup{}

这里有3个东西:

  1. type IRouter interface:一个IRouter接口,包含了所有的单个路由和路由组

  2. type IRoutes interface:定义了一些请求的接口,包括中间件,http请求和文件服务器相关

  3. type RouterGroup struct:一个路由组结构体

  4. Handlers HandlersChain:一个处理回调的Handler切片

  5. basePath string:分组的基础路由

  6. engine *Engine:注入的engine

  7. root bool:是不是一个跟路由分组

从routergroup包中的内容来看,主要的变量就是 RouterGroup 这个结构体了,既然成员没有说明多少东西,那么查看一下包中的方法:

// 用来注册中间件
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {}

// 用于返回一个新的分组,也就是可以做一个分组基类,里面包含所有分组都需要调用的中间件
// 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 {}

// 返回路由组的基础路径
// BasePath returns the base path of router group.
// For example, if v := router.Group("/rest/n/v1/api"), v.BasePath() is "/rest/n/v1/api".
func (group *RouterGroup) BasePath() string {}

// 真正的注册请求的方法,计算了绝对路径后和路由组的中间件合并了然后注册了路由
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()
}

// 用来注册回调组的,只有最后一个回调是真正的响应请求,其他的应该是中间件,这里是用来注册一些自定义的方法的,对于 GET, POST, PUT, PATCH and DELETE 方法有另外给出
// 代码留下来了,这里是判断了请求是否符合要求后使用了私有的handle方法
// Handle registers a new request handle and middleware with the given path and method.
// The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes.
// See the example code in GitHub.
//
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
// functions can be used.
//
// This function is intended for bulk loading and to allow the usage of less
// frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy).
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
	if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
		panic("http method " + httpMethod + " is not valid")
	}
	return group.handle(httpMethod, relativePath, handlers)
}

// 注册一个POST请求
// POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {}

// 注册一个GET请求
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {}

// 注册一个DELETE请求
// DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {}

// 注册一个PATCH请求
// PATCH is a shortcut for router.Handle("PATCH", path, handle).
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {}

// 注册一个PUT请求
// PUT is a shortcut for router.Handle("PUT", path, handle).
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {}

// 注册一个OPTIONS请求
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle).
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {}

// 注册一个head请求
// HEAD is a shortcut for router.Handle("HEAD", path, handle).
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {}

// 注册一个可以响应所有http请求方式的路由
// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {}

// 用来响应单个文件的本地文件服务的路由
// StaticFile registers a single route in order to serve a single file of the local filesystem.
// router.StaticFile("favicon.ico", "./resources/favicon.ico")
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {=}

// 静态文件的请求的路由
// Static serves files from the given file system root.
// Internally a http.FileServer is used, therefore http.NotFound is used instead
// of the Router's NotFound handler.
// To use the operating system's file system implementation,
// use :
//     router.Static("/static", "/var/www")
func (group *RouterGroup) Static(relativePath, root string) IRoutes {}

// 创建一个自定义的文件系统服务,想要创建默认的文件系统可以使用 gin.Dir()
// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead.
// Gin by default user: gin.Dir()
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes {}

// 创建文件服务系统的回调
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {}

// 用来合并传入的 HandlersChain 和group内的 HandlersChain
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {}

// 用来计算路由的
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {}

// 返回一个 IRoutes ,这里直接返回了 RouterGroup
func (group *RouterGroup) returnObj() IRoutes {
	if group.root {
		return group.engine
	}
	return group
}

总结一下 RouterGroup 里包含的方法:

  1. 使用 use 来注册中间件

  2. 需要注意的是,在注册请求回调的时候是将路由分组中的中间件拷贝了一份到路由回调中,那么 use 使用的位置就值的注意了,use 注册中间件在注册请求回调后面是否会丢失中间件呢?这里先留下一个疑问,本篇结束的时候测试一下。

  3. 使用 Group 来创建一个子 RouterGroup ,一般是用来做全局中间件或父子中间件的

  4. 使用 Handle 来注册一个自定义方式的请求

  5. 正常的http请求直接使用 GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE 即可

  6. 也可以使用 Any 来注册一个可以同时相应所有http请求方式的回调 GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.

  7. 文件相关的路由如下:

  8. StaticFile 用来注册单个文件请求的路由

  9. Static 快速启动一个文件服务器

  10. StaticFS 启动一个自定义的文件服务器

看完了 RouterGroup 里的内容,下面看看注册回调 handle 到底都做了什么

handle

上面留下了 RouterGroup.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()
}
  1. absolutePath 通过分组计算了绝对路由
  2. handlers 通过分组将中间件和回调的handle组合到了一个切片中
  3. group.engine.addRoute 增加一个路由

这里需要注意看的只有 group.engine.addRoute,已经到增加路由了,感觉离胜利不远了。

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)
	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)
}

一些基本的操作:

  1. 路径毕竟以/开始
  2. 请求方式不能为空
  3. 至少有一个回调
  4. debug了一些日志

root := engine.trees.get(method) 这里开始到了重要的地方了,还记得 engine.tree 是什么吗?它是一个 methodTree 结构体切片,里面包含了 method string 请求方式和 root *node 一个node节点。

engine.trees.get(method) 遍历了这个tree找到了对应请求方式的节点。这个节点是一个树状结构的:

type node struct {
	path      string
	indices   string
	children  []*node // 节点中包含许多子节点
	handlers  HandlersChain
	priority  uint32
	nType     nodeType
	maxParams uint8
	wildChild bool
	fullPath  string
}

然后使用 root.addRoute 将回调注册到了节点中。这里代码比较长久不贴了,想看的话可以自行找源码。

到这里,官方demo的默认框架引擎源码追踪就可以告一段落了。总结一下都学到了什么:

  1. 使用 engine := New() 实例化一个自定义的框架实体,实体中今天提到的两个东西是RouterGroup
  2. RouterGroup 包含了路由的分组,回调注册,中间件等信息
  3. HandlersChain 一个回调方法的切片,其中最后一个handler是真正的处理请求的函数,其他的都是中间件
  4. 设置子分组后会自动复制一份父分组的 HandlersChain,也就是说子分组内自带了父分组的中间件
  5. 使用 use 来注册中间件
  6. 使用 Group 来创建一个子 RouterGroup ,一般是用来做全局中间件或父子中间件的
  7. 使用 Handle 来注册一个自定义方式的请求
  8. 正常的http请求直接使用 GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE 即可
  9. 也可以使用 Any 来注册一个可以同时相应所有http请求方式的回调 GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.

最后的小实验

上面有说到自己的一个疑问:

在注册请求回调的时候是将路由分组中的中间件拷贝了一份到路由回调中,那么 use 使用的位置就值的注意了,use 注册中间件在注册请求回调后面是否会丢失中间件呢?这里先留下一个疑问,本篇结束的时候测试一下。

这里来尝试一下顺便注册一个自定义的框架实例:

func main() {
	r := gin.New()
	r.Use(func(context *gin.Context) {
		log.Println("test 1")
	})
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Use(func(context *gin.Context) {
		log.Println("test 2")
	})
	r.GET("/", func(context *gin.Context) {
		context.JSON(200, gin.H{"message": "hello world"})
	})
	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
C:\project\gin
λ 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    /ping                     --> main.main.func2 (2 handlers)
[GIN-debug] GET    /                         --> main.main.func4 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

从命令行中的debug日志就能看出来:

  1. [GIN-debug] GET /ping --> main.main.func2 (2 handlers) /ping 只有2个回调

  2. [GIN-debug] GET / --> main.main.func4 (3 handlers) / 有3个回调

这其中多出来的一个就是后面注册的中间件了,那么访问一下试试:

C:\project
λ curl 127.0.0.1:8080/
{"message":"hello world"}

C:\project
λ curl 127.0.0.1:8080/ping
{"message":"pong"}
2020/03/23 18:14:45 test 1 /
2020/03/23 18:14:45 test 2 /
2020/03/23 18:14:50 test 1 /ping

猜测是正确的,所以在开发中需要注意中间件注册的位置,也可以根据这个特性在一个分组中使用不同的中间件。不过这是一个错误的操作,请不要这样做,这样gin作者开发的分组功能就很尴尬了,而且这样会导致分组内的路由中间件不一致,代码阅读和debug会出现一些问题

接下来的内容

接下来大概会有对 engin.run() 的阅读,对 context 的阅读和模板包template的阅读,最后会对手册进行详细阅读。

程序幼儿员-龚学鹏
请先登录后发表评论
  • latest comments
  • 总共0条评论