Gin框架入门
本文是对开启博客之路 | Go 语言编程之旅 (eddycjy.com)的学习。
详细分析:Go框架解析:gin - TIGERB
Gin框架启动的一个简单HTTP服务器;
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.Run()
}
运行结果如下:
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
...
[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
对运行结果做一个初步的概括分析,分为以下四大块:
- 默认 Engine 实例:当前默认使用了官方所提供的 Logger 和 Recovery 中间件创建了 Engine 实例。
- 运行模式:当前为调试模式,并建议若在生产环境时切换为发布模式。
- 路由注册:注册了
GET /ping
的路由,并输出其调用方法的方法名。 - 运行信息:本次启动时监听 8080 端口,由于没有设置端口号等信息,因此默认为 8080。
分析
整体流程,如下图所示:
gin.Default
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
-
debugPrintWARNINGDefault()
:调试模式日志输出-
func debugPrintWARNINGDefault() { if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { debugPrint(`[WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon. `) } debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. `) }
-
在调用
debugPrintWARNINGDefault
方法时,会检查 Go 版本是否达到 gin 的最低要求,再进行调试的日志[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
的输出,用于提醒开发人员框架内部已经默认检查和集成了缺省值。
-
-
通过调用
gin.Default
方法来创建默认的 Engine 实例,它会在初始化阶段就引入 Logger 和 Recovery 中间件,能够保障你应用程序的最基本运作,这两个中间件具有以下作用:- Logger:输出请求日志,并标准化日志的格式。
- Recovery:异常捕获,也就是针对每次请求处理进行 recovery 处理,防止因为出现 panic 导致服务崩溃,并同时标准化异常日志的格式。
gin.New
New
方法返回的是HTTP服务的核心引擎;
func New() *Engine {
debugPrintWARNINGNew() // 调试模式日志输出
engine := &Engine{
RouterGroup: RouterGroup{ // 给框架实例绑定上一个路由组
Handlers: nil, // engine.Use 注册的中间方法到这里
basePath: "/",
root: true, // 是否是路由根节点
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9), // 路由树,路由最终注册到了这里
delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);",
}
// RouterGroup绑定engine自身的实例
// 不太明白为何如此设计,职责分明么?
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} { // 绑定从实例池获取上下文的闭包方法
return engine.allocateContext() // 获取一个Context实例
}
return engine
}
- 为什么叫gin呢?这个框架实例的结构体实际命名是Engine,很明显gin就是一个很个性的简称了。Go框架解析:gin - TIGERB
- RouterGroup:路由组,所有的路由规则都由
*RouterGroup
所属的方法进行管理,在 gin 中和 Engine 实例形成一个重要的关联组件。 - RedirectTrailingSlash:是否自动重定向,如果启用了,在无法匹配当前路由的情况下,则自动重定向到带有或不带斜杠的处理程序去。例如:当外部请求了
/tour/
路由,但当前并没有注册该路由规则,只有/tour
的路由规则时,将会在内部进行判定,若是 HTTP GET 请求,将会通过 HTTP Code 301 重定向到/tour
的处理程序去,但若是其他类型的 HTTP 请求,那么将会是以 HTTP Code 307 重定向。,通过指定的 HTTP 状态码重定向到/tour
路由的处理程序去。 - RedirectFixedPath:是否尝试修复当前请求路径,也就是在开启的情况下,gin 会尽可能的帮你找到一个相似的路由规则并在内部重定向过去,主要是对当前的请求路径进行格式清除(删除多余的斜杠)和不区分大小写的路由查找等。
- HandleMethodNotAllowed:判断当前路由是否允许调用其他方法,如果当前请求无法路由,则返回 Method Not Allowed(HTTP Code 405)的响应结果。如果无法路由,也不支持重定向其他方法,则交由 NotFound Hander 进行处理。
- ForwardedByClientIP:如果开启,则尽可能的返回真实的客户端 IP,先从 X-Forwarded-For 取值,如果没有再从 X-Real-Ip。(不理解)
- UseRawPath:如果开启,则会使用
url.RawPath
来获取请求参数,不开启则还是按 url.Path 去获取。(不理解) - UnescapePathValues:是否对路径值进行转义处理。
- MaxMultipartMemory:相对应
http.Request ParseMultipartForm
方法,用于控制最大的文件上传大小。 - trees:多个压缩字典树(Radix Tree),每个树都对应着一种 HTTP Method。你可以理解为,每当你添加一个新路由规则时,就会往 HTTP Method 对应的那个树里新增一个 node 节点,以此形成关联关系。
- delims:用于 HTML 模板的左右定界符。
r.GET
// 注册GET请求路由
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
// 往路由组内 注册GET请求路由
return group.handle("GET", relativePath, handlers)
}合并现有和新注册的 Handler,并创建一个函数链 HandlersChain。
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
// 把中间件的handle和该路由的handle合并
handlers = group.combineHandlers(handlers)
// 注册一个GET集合的路由
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
-
handlers ...HandlerFunc
:类型前面三个点,表示可边长参数,实际类型是切片。 -
计算路由的绝对路径,也就是
group.basePath
与我们定义的路由路径组装,那么 group 又是什么东西呢,实际上在 gin 中存在组别路由的概念,这个知识点在后续实战中会使用到。 -
合并现有和新注册的 Handler,并创建一个函数链 HandlersChain。
-
将当前注册的路由规则(含 HTTP Method、Path、Handlers)追加到对应的树中。
-
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) // 检查有没有对应method集合的路由 root := engine.trees.get(method) if root == nil { // 没有 创建一个新的路由节点 root = new(node) // 添加该method的路由tree到当前的路由到路由树里 engine.trees = append(engine.trees, methodTree{method: method, root: root}) } // 添加路由 root.addRoute(path, handlers) }
-
[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
为什么这条调试信息的最后,显示的是 3 handlers
?明明我们只注册了 /ping
这一条路由而已,是不是应该是一个 Handler。其实不然,我们看看上述创建函数链 HandlersChain 的详细步骤,就知道为什么了,如下:
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
在 combineHandlers
方法中,最终函数链 HandlersChain 的是由 group.Handlers
和外部传入的 handlers
组成的,从拷贝的顺序来看,group.Handlers
的优先级高于外部传入的 handlers
。
结合 Use
方法来看,很显然是在 gin.Default
方法中注册的中间件影响了这个结果,因为中间件也属于 group.Handlers
的一部分,也就是在调用 gin.Use
,就已经注册进去了,如下:
engine.Use(Logger(), Recovery())
...
// engine.Use 会调用 engine.RouterGroup.Use(middleware...)
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
因此所注册的路由加上内部默认设置的两个中间件,最终使得显示的结果为 3 handlers
。
r. Run
支撑实际运行 HTTP Server 的 Run 方法,
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
// 执行http包的ListenAndServe方法 启动路由
// engine实现了http.Handler接口 所以在这里作为参数传参进去
// 后面我们再看engine.ServeHTTP的具体逻辑
err = http.ListenAndServe(address, engine)
return
}
...
⬇️
// 下面就是网络相关了
// 监听IP+端口
ln, err := net.Listen("tcp", addr)
⬇️
// 上面执行完了监听
// 接着就是Serve
srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
⬇️
// Accept请求
rw, e := l.Accept()
⬇️
// 使用goroutine去处理一个请求
// 最终就执行的是engine的ServeHTTP方法
go c.serve(ctx)
该方法会通过解析地址,再调用 http.ListenAndServe
将 Engine 实例作为 Handler
注册进去,然后启动服务,开始对外提供 HTTP 服务。
func ListenAndServe(addr string, handler Handler) error {}
...
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
这里值得关注的是,为什么 Engine 实例能够传进去呢,明明形参要求的是 Handler
接口类型?
- 在 Go 语言中如果某个结构体实现了 interface 定义声明的那些方法,那么就可以认为这个结构体实现了 interface。
// 接着我们来看看engine的ServeHTTP方法的具体内容
// engine实现http.Handler接口ServeHTTP的具体方法
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 获取一个上下文实例
// 从实例池获取性能高
c := engine.pool.Get().(*Context)
c.writermem.reset(w) // 重置获取到的上下文实例的http.ResponseWriter
c.Request = req // 重置获取到的上下文实例*http.Request
c.reset() // 重置获取到的上下文实例的其他属性
// 实际处理请求的地方
// 传递当前的上下文
engine.handleHTTPRequest(c)
//归还上下文实例
engine.pool.Put(c)
}
- 从
sync.Pool
对象池中获取一个上下文对象。 - 重新初始化取出来的上下文对象。
- 处理外部的 HTTP 请求。
- 处理完毕,将取出的上下文对象返回给对象池。
这里的池化不懂,主要是sync.Poold
是什么?
?在这里上下文的池化主要是为了防止频繁反复生成上下文对象,相对的提高性能,并且针对 gin 本身的处理逻辑进行二次封装处理。
中间件
此处需要对“中间件”的概念进行一个说明,因为在分布式系统中也遇到了中间件:是一类提供系统软件和应用软件之间连接、便于软件各部件之间的沟通的软件,应用软件可以借助中间件在不同的技术架构之间共享信息与资源。中间件位于客户机服务器的操作系统之上,管理着计算资源和网络通信。(维基百科)·
评判是否是中间件的关键在于:
- 性质:中间件是软件。
- 作用层级:系统软件和应用软件之间、软件各部件之间;管理客户机与系统软件之间的计算资源和网络通信。
- 服务对象:中间件为应用软件服务,应用软件为最终用户服务,最终用户并不直接使用中间件。
也可以理解中间件为:将具体业务和底层逻辑解耦的组件。
参考链接:中间件是什么?https://www.zhihu.com/question/19730582/answer/1663627873