上一篇中安装并通过官方提供的defaultdemo大概了解了一下gin,现在开始深入阅读gin的源码吧!
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行。
慢慢看。先看第一个: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
方法返回了一个默认的框架实体
打印日志用的,跳过他
这里实例化了一个框架实体
// 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 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中的成员如下:
/
,当一个路由如 /xxx/
找不到匹配时,配置此选项为true后会自动重定向尝试 /xxx
是否可以匹配,其中对于 get
请求返回301,其他请求返回307//
并可以将路由变为全小写格式X-AppEngine...
url.RawPath
去寻找请求参数url.Path
使用的,不转义请求路径Multipart
类型的表单大小(用来限制上传文件大小的)Context.SecureJSON
中使用的,先扔在这吧上面的内容中,关于模板的渲染器 template
包还没有说过,后面再看吧。如果一个项目需要用后端去渲染页面的话,为什么不用php呢?
上面这么多成员,最重要的还是 RouterGroup
路由分组,在路由分组中我们应该可以看到路由匹配相关的内容,这也是一个web框架的灵魂之一。第二可以看看用来注册请求回调的HandlersChain
长什么样。
首先看看 HandlersChain
吧,router
可能涉及到的内容应该会比请求回调多,先看简单的。
// 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
参数,也就是上一篇说到的上下文,在上一篇也看过了,上下文主要是保存了请求和响应的实例以及请求的参数等本次请求的信息。
现在再来看看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个东西:
type IRouter interface
:一个IRouter接口,包含了所有的单个路由和路由组
type IRoutes interface
:定义了一些请求的接口,包括中间件,http请求和文件服务器相关
type RouterGroup struct
:一个路由组结构体
Handlers HandlersChain
:一个处理回调的Handler切片
basePath string
:分组的基础路由
engine *Engine
:注入的engine
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
里包含的方法:
使用 use
来注册中间件
需要注意的是,在注册请求回调的时候是将路由分组中的中间件拷贝了一份到路由回调中,那么 use
使用的位置就值的注意了,use
注册中间件在注册请求回调后面是否会丢失中间件呢?这里先留下一个疑问,本篇结束的时候测试一下。
使用 Group
来创建一个子 RouterGroup
,一般是用来做全局中间件或父子中间件的
使用 Handle
来注册一个自定义方式的请求
正常的http请求直接使用 GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE
即可
也可以使用 Any
来注册一个可以同时相应所有http请求方式的回调 GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
文件相关的路由如下:
StaticFile 用来注册单个文件请求的路由
Static 快速启动一个文件服务器
StaticFS 启动一个自定义的文件服务器
看完了 RouterGroup
里的内容,下面看看注册回调 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()
}
这里需要注意看的只有 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)
}
一些基本的操作:
/
开始从 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的默认框架引擎源码追踪就可以告一段落了。总结一下都学到了什么:
engine := New()
实例化一个自定义的框架实体,实体中今天提到的两个东西是RouterGroup
RouterGroup
包含了路由的分组,回调注册,中间件等信息HandlersChain
一个回调方法的切片,其中最后一个handler是真正的处理请求的函数,其他的都是中间件HandlersChain
,也就是说子分组内自带了父分组的中间件上面有说到自己的一个疑问:
在注册请求回调的时候是将路由分组中的中间件拷贝了一份到路由回调中,那么 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日志就能看出来:
[GIN-debug] GET /ping --> main.main.func2 (2 handlers) /ping 只有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
的阅读,最后会对手册进行详细阅读。
本文为龚学鹏原创文章,转载无需和我联系,但请注明来自龚学鹏博客http://www.noobcoder.cn
最新评论