![Go语言编程之旅:一起用Go做项目](https://wfqqreader-1252317822.image.myqcloud.com/cover/485/32441485/b_32441485.jpg)
第2章 HTTP应用:写一个完整的博客后端
2.1.1 gin
本次博客项目将选用gin框架进行开发。gin是用Go语言编写的一个HTTP Web框架,它具有类似于Martini的API风格,并且使用了著名的开源项目httprouter的自定义版本作为路由基础,与Martini相比,性能大约提高了40倍。
另外,gin除了快,还具备小巧、精美且易用的特性,广受Go语言开发者的喜爱,是最流行的HTTP Web框架(从GitHub Star上来看)。
注意:框架仅仅只是一个“工具”,我们不应过度局限于此,而是要尽可能地学习其原理和思路。实际上,本文所实现的功能在任何框架上都能实现,因此学懂它非常重要,这也是笔者所倡导的。
2.1.3 安装gin
安装gin的相关模块,执行如下命令:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_51_2.jpg?sign=1739654408-pERe9F6NBSROK0oXHpTCzUNRrt0SgxAz-0-f8d3e168afb865fa3c825291c82df29f)
go.mod文件的内容也相应进行了变更,打开go.mod文件:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_52_1.jpg?sign=1739654408-6uWUflZrZKlRF4LJ0Q5wRNfv3RPNegDJ-0-738431f8057aeabc58baf8294c027a6e)
这些就是gin关联的所有模块包。为什么github.com/gin-gonic/gin后面会出现indirect标识,它不是直接通过调用go get引用的吗?其实不然,因为在安装时,这个项目模块还没有真正地使用它(Go modules会根据项目下的依赖情况来决定)。
另外,在go.mod文件中有类似go 1.14的标识位,目前来看,暂时没有明确的实际作用,主要与创建Go modules时的Go版本有关。
2.1.4 快速启动
在完成前置动作后,本节首先运行一个 Demo,看看一个最简单的 HTTP 服务运行起来是什么样的。在blog-service的项目根目录下新建一个main.go文件,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_52_2.jpg?sign=1739654408-dgnk71oQKhwiZrZ16oTIvOaA29EeKcnl-0-c2604e760664bbb1d5de89a41110aec2)
接下来运行main.go文件,查看运行结果:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_52_3.jpg?sign=1739654408-AxSS4GQLzfc3PDHXxP32ZrS9Fu2yRGmu-0-f05f09cb8c1c93c62ca1d8a4d98732a8)
可以看到,在启动服务后,输出了许多运行信息。下面我们对运行信息做一个初步的概括分析,把它分为四大块:
● 默认Engine实例:表示默认使用官方提供的Logger和Recovery中间件创建Engine实例。
● 运行模式:表示当前为调试模式,建议在生产环境时切换为发布模式。
● 路由注册:表示注册了GET/ping的路由,并输出了其调用方法的方法名。
● 运行信息:表示本次启动时监听 8080 端口,由于没有设置端口号等信息,因此默认为8080。
2.1.5 验证
在启动之后,这个服务就开始对外提供服务了,我们只需针对所配置的端口号和设置的路由规则进行请求,就可以得到响应结果,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_53_1.jpg?sign=1739654408-WX6VW74xWQzje3HcFtzeEOZBAzfJ7czo-0-fe1890f2f01812f184cfd7c83155cb68)
响应结果与预期一致,表示该服务运行正确。
2.1.6 源码分析
只是通过简单的几段代码,就能完成一个“强劲”的HTTP服务。那么,底层的处理逻辑是什么,一些服务端参数是在哪里设置的,那么多的调试信息又是从哪里输出的,能不能关掉呢?接下来我们就对源码进行大体分析,对这些问题一探究竟,简单地解剖一下里面的秘密,整体分析流程如图2-1所示。
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_53_2.jpg?sign=1739654408-tRI0pLC3DVY7SfAFjnaIS4Va10t398mB-0-52d21a32cb14defece75d5f5d149d8ec)
图2-1
1.gin.Default
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_54_1.jpg?sign=1739654408-XYfOQKq4JYJp4oprZjU4xgFTP71E1wAy-0-3fde3d71598f2ccc78c13ef95231941b)
通过调用 gin.Default 方法创建默认的 Engine 实例,它会在初始化阶段引入 Logger 和Recovery中间件,保障应用程序最基本的运作。这两个中间件的作用如下。
● Logger:输出请求日志,并标准化日志的格式。
● Recovery:异常捕获,也就是针对每次请求处理进行 Recovery 处理,防止因为出现panic导致服务崩溃,同时将异常日志的格式标准化。
另外,在调用debugPrintWARNINGDefault方法时,首先会检查Go版本是否达到gin的最低要求,然后再调试日志[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.的输出,以此提醒开发人员框架内部已经开始检查并集成了默认值。
2.gin.New
New方法是最重要的方法,因为它会对Engine实例执行初始化动作并返回,在gin中承担了“主轴”的作用。在初始化时需要设置哪里参数,是否会影响日常开发呢?下面继续深入探究,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_54_2.jpg?sign=1739654408-CNHtfFhp3khTMGux9ctu4v3PQvTsQGmG-0-2e99e7239ece06ecd7df71088192e5e7)
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_55_1.jpg?sign=1739654408-uv4f94bsNMLOlqtO5QmvGIlO890tlbzd-0-39ee79fb63ec036c38442f47ed5bc62f)
● RouterGroup:路由组。所有的路由规则都由*RouterGroup 所属的方法进行管理。在gin中,路由组和Engine实例形成了一个重要的关联组件。
● RedirectTrailingSlash:是否自动重定向。如果启用,在无法匹配当前路由的情况下,则自动重定向到带有或不带斜杠的处理程序中。例如,当外部请求了/tour/路由,但当前并没有注册该路由规则,而只有/tour 的路由规则时,将会在内部进行判定。若是HTTP GET请求,则会通过HTTP Code 301重定向到/tour的处理程序中;若是其他类型的HTTP请求,则会以HTTP Code 307重定向,通过指定的HTTP状态码重定向到/tour路由的处理程序中。
● RedirectFixedPath:是否尝试修复当前请求路径,也就是在开启的情况下,gin会尽可能地找到一个相似的路由规则,并在内部重定向。RedirectFixedPath 的主要功能是对当前的请求路径进行格式清除(删除多余的斜杠)和不区分大小写的路由查找等。
● 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模板的左右定界符。
总体来讲,Engine实例就像引擎一样,与整个应用的运行、路由、对象、模板等管理和调度都有关联。另外,通过上述解析可以发现,其实 gin 在初始化时默认已经做了很多事情,可以说是既定了一些默认运行基础。
3.r.GET
在注册路由时,使用了r.GET方法将定义的路由注册进去,下面一起看看它到底注册了什么,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_56_1.jpg?sign=1739654408-fdZpPW1BqwItJKb0C1nZ6pm0ig7zdP9B-0-4f3c7c6120395812403592579ff167bb)
● 计算路由的绝对路径,即 group.basePath 与我们定义的路由路径组装。group 是什么呢?实际上,在gin中存在组别路由的概念,这个知识点在后续实战中会用到。
● 合并现有的和新注册的Handler,并创建一个函数链HandlersChain。
● 将当前注册的路由规则(含HTTP Method、Path和Handlers)追加到对应的树中。
这类方法主要针对路由的各类计算和注册行为,并输出路由注册的调试信息,如运行时的路由信息:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_56_2.jpg?sign=1739654408-y9yEHjplfJW6k03Lb6aIQqikXJzoQZ2y-0-82c37e1e8dad46ba902d0fab7588cc94)
另外,在这条调试信息的最后,显示的是3 handlers。为什么是3 handlers呢?明明只注册了/ping一条路由,不应是1handler吗?其实不然,仔细观察创建函数链HandlersChain的详细步骤,就知道为什么了,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_56_3.jpg?sign=1739654408-zSxxVWgW7HJrx1ZhQExXArH5VjR39Kol-0-472e9e8f129f206a4f284dda56293d0d)
从代码中可以看出,在 combineHandlers 方法中,最终函数链 HandlersChain 是由group.Handlers和外部传入的handlers组成的,从拷贝的顺序来看,group.Handlers的优先级高于外部传入的handlers。
以此再结合Use方法来看,很显然是在gin.Default方法中注册的中间件影响了这个结果。因为中间件也属于group.Handlers 的一部分,也就是在调用gin.Use时,就已经注册进去了,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_57_1.jpg?sign=1739654408-5n2RDg1GDCT4rA5KnCC11MXuTo0N86BL-0-749717d9aedd638e9d72392012d3b3ce)
因此,我们所注册的路由加上内部默认设置的两个中间件,最终使得显示的结果为 3 handlers。
4.r.Run
下面一起看看支撑实际运行HTTP Server的Run方法都做了哪些事情,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_57_2.jpg?sign=1739654408-gtfe3xIwqbfrUZH4UhWH5XB0r0Lp7ddP-0-7b95ce3622da48fbe253243ea46d3ba7)
该方法会通过解析地址,再调用http.ListenAndServe,将Engine实例作为Handler注册进去,然后启动服务,开始对外提供HTTP服务。
值得注意的是,明明形参要求的是Handler接口类型,为什么Engine实例能够传进去呢?实际上在Go语言中,如果某个结构体实现了interface定义声明的那些方法,那么就可以认为这个结构体实现了interface。
在gin中,Engine结构体确实实现了ServeHTTP方法,即符合http.Handler接口标准,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_57_3.jpg?sign=1739654408-V1yr9KoFwgkHTRccL9JccBLklEm54E9V-0-7da53c3c5bffb25182a9c0135a14fdfa)
● 从sync.Pool对象池中获取一个上下文对象。
● 重新初始化取出来的上下文对象。
● 处理外部的HTTP请求。
● 处理完毕,将取出的上下文对象返回给对象池。
在这里,上下文的池化主要是为了防止频繁反复生成上下文对象,相对地提高性能,并且针对gin本身的处理逻辑进行二次封装处理。
2.1.7 小结
本节介绍了Go语言中比较流行的gin框架,并且使用它完成了一个简单的HTTP Server。同时我们还基于示例代码,对其进行了源码分析。作为一个开发人员,应不仅做到会用,还应了解它的底层实现原理。知道做了什么默认设置,输出的调试信息为何与最初期望的不一样,尽可能地做到知其然并知其所以然。只有这样才能更好地使用它,而不是被使用。