
第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的相关模块,执行如下命令:

go.mod文件的内容也相应进行了变更,打开go.mod文件:

这些就是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文件,代码如下:

接下来运行main.go文件,查看运行结果:

可以看到,在启动服务后,输出了许多运行信息。下面我们对运行信息做一个初步的概括分析,把它分为四大块:
● 默认Engine实例:表示默认使用官方提供的Logger和Recovery中间件创建Engine实例。
● 运行模式:表示当前为调试模式,建议在生产环境时切换为发布模式。
● 路由注册:表示注册了GET/ping的路由,并输出了其调用方法的方法名。
● 运行信息:表示本次启动时监听 8080 端口,由于没有设置端口号等信息,因此默认为8080。
2.1.5 验证
在启动之后,这个服务就开始对外提供服务了,我们只需针对所配置的端口号和设置的路由规则进行请求,就可以得到响应结果,代码如下:

响应结果与预期一致,表示该服务运行正确。
2.1.6 源码分析
只是通过简单的几段代码,就能完成一个“强劲”的HTTP服务。那么,底层的处理逻辑是什么,一些服务端参数是在哪里设置的,那么多的调试信息又是从哪里输出的,能不能关掉呢?接下来我们就对源码进行大体分析,对这些问题一探究竟,简单地解剖一下里面的秘密,整体分析流程如图2-1所示。

图2-1
1.gin.Default

通过调用 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中承担了“主轴”的作用。在初始化时需要设置哪里参数,是否会影响日常开发呢?下面继续深入探究,代码如下:


● 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方法将定义的路由注册进去,下面一起看看它到底注册了什么,代码如下:

● 计算路由的绝对路径,即 group.basePath 与我们定义的路由路径组装。group 是什么呢?实际上,在gin中存在组别路由的概念,这个知识点在后续实战中会用到。
● 合并现有的和新注册的Handler,并创建一个函数链HandlersChain。
● 将当前注册的路由规则(含HTTP Method、Path和Handlers)追加到对应的树中。
这类方法主要针对路由的各类计算和注册行为,并输出路由注册的调试信息,如运行时的路由信息:

另外,在这条调试信息的最后,显示的是3 handlers。为什么是3 handlers呢?明明只注册了/ping一条路由,不应是1handler吗?其实不然,仔细观察创建函数链HandlersChain的详细步骤,就知道为什么了,代码如下:

从代码中可以看出,在 combineHandlers 方法中,最终函数链 HandlersChain 是由group.Handlers和外部传入的handlers组成的,从拷贝的顺序来看,group.Handlers的优先级高于外部传入的handlers。
以此再结合Use方法来看,很显然是在gin.Default方法中注册的中间件影响了这个结果。因为中间件也属于group.Handlers 的一部分,也就是在调用gin.Use时,就已经注册进去了,代码如下:

因此,我们所注册的路由加上内部默认设置的两个中间件,最终使得显示的结果为 3 handlers。
4.r.Run
下面一起看看支撑实际运行HTTP Server的Run方法都做了哪些事情,代码如下:

该方法会通过解析地址,再调用http.ListenAndServe,将Engine实例作为Handler注册进去,然后启动服务,开始对外提供HTTP服务。
值得注意的是,明明形参要求的是Handler接口类型,为什么Engine实例能够传进去呢?实际上在Go语言中,如果某个结构体实现了interface定义声明的那些方法,那么就可以认为这个结构体实现了interface。
在gin中,Engine结构体确实实现了ServeHTTP方法,即符合http.Handler接口标准,代码如下:

● 从sync.Pool对象池中获取一个上下文对象。
● 重新初始化取出来的上下文对象。
● 处理外部的HTTP请求。
● 处理完毕,将取出的上下文对象返回给对象池。
在这里,上下文的池化主要是为了防止频繁反复生成上下文对象,相对地提高性能,并且针对gin本身的处理逻辑进行二次封装处理。
2.1.7 小结
本节介绍了Go语言中比较流行的gin框架,并且使用它完成了一个简单的HTTP Server。同时我们还基于示例代码,对其进行了源码分析。作为一个开发人员,应不仅做到会用,还应了解它的底层实现原理。知道做了什么默认设置,输出的调试信息为何与最初期望的不一样,尽可能地做到知其然并知其所以然。只有这样才能更好地使用它,而不是被使用。