这是我参与「第三届青训营 -后端场」笔记创作活动的的第四篇笔记
前言
我们先用go中的net/http包实现web服务
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/hello", handler)
err := http.ListenAndServe(":9090", nil)
if err != nil {
fmt.Println("http server failed, err:%v \n", err)
return
}
}
// http.ResponseWriter:代表响应,传递到前端的
// *http.Request:表示请求,从前端传递过来的
func handler(w http.ResponseWriter, r *http.Request) {
str := "hello Golang!"
fmt.Fprintln(w, str);
}
在浏览器访问如下地址
http://localhost:9090/hello
就能打开我们的hello golang页面了
我们可以给文字添加色彩
// http.ResponseWriter:代表响应,传递到前端的
// *http.Request:表示请求,从前端传递过来的
func handler(w http.ResponseWriter, r *http.Request) {
str := "<h1 style='color:red'>hello Golang!<h1>"
fmt.Fprintln(w, str);
}
然后重启后,在刷新
我们还可以把里面的字符串放在一个文件里,我们定义一个 hello.html文件
<html>
<title>hello golang</title>
<body>
<h1 style="color: red">
hello Golang!
</h1>
<h1>
hello gin!
</h1>
<img src="https://photo69.macsc.com/180429/180429_180/yXgMGTMOHn_small.jpg">
</body>
</html>
然后修改刚刚的main.go,使用 ioutil解析文件
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
http.HandleFunc("/hello", handler)
err := http.ListenAndServe(":9090", nil)
if err != nil {
fmt.Printf("http server failed, err:%v", err)
}
}
// http.ResponseWriter:代表相应,传递到前端
// http.Request: 表示请求,从前端传递过来的
func handler(w http.ResponseWriter, r *http.Request) {
//str := "<h1 style='color:red'>hello Golang!<h1>"
html, _ := ioutil.ReadFile("hello.html")
fmt.Fprintln(w, string(html))
}
刷新刚刚的页面
我们通过上面的http包,就能够实现一个web的开发,那为什么还要用gin呢?
其实框架的好处,就是别人帮我们搭建了一个舞台,同时提供了很多现成的轮子,让我们专注于业务的开发,同时让开发效率更高。
HTTP框架的设计与实现
分层设计
从上到下依次是:业务层、中间件层(服务治理层)、路由层、协议编(解)码层、网络传输层
一个切实可行的复杂系统势必使从一个切实可行的简单系统发展而来。从头开始设计的复杂系统根本不切实可行,无法修修补补让它切实可行。你必须由一个切实可行的简单系统重新开始。
——盖尔定律
应用层设计
内容: Context, Request, Response, Handler
设计准则:
-
提供合理的API
- 可理解性:如
ctx.Body(),ctx.GetBody()‘;而不是ctx.BodyA() - 简单性:如
ctx.Request.Header.Peek(key)可以根据使用场景优化为ctx.GetHeader(key)
- 可理解性:如
不要试图在文档中去说明,很多用户并不看
中间件设计
内容:e.g. Recovey, CircuitBreak, Timeout, Access log
设计准则:
- 配合Handler实现一个完整的请求处理生命周期
- 拥有预处理逻辑和后处理逻辑
- 可以注册多个中间件
- 对上层模块用户逻辑模块易用
洋葱模型
核心逻辑与通用逻辑分离:
例子:打印每个请求的request和response
- 没有使用中间件之前
func main() {
h := server.New();
h.POST("/login", func(c context.Context, ctx *app.RequestContext){
// print request
logs.Infof("Received RawRequest: %s", ctx.Request.RawRequest())
// some biz logic
ctx.JSON(200, "OK")
// print response
logs.Infof("Send RawResponse: %s", ctx.Reponse.RawResponse())
})
h.POST("/logout", func(c context.Context, ctx *app.RequestContext){
// print request
logs.Infof("Received RawRequest: %s", ctx.Request.RawRequest())
// some biz logic
ctx.JSON(200, "OK")
// print response
logs.Infof("Send RawResponse: %s", ctx.Reponse.RawResponse())
})
}
同一个函数我们输入了两遍 -> 代码冗余,不够美观
- 使用中间件之后
func main() {
h := server.New();
h.Use(func(c context.Context, ctx *app.RequestContext){
// print request
logs.Infof("Received RawRequest: %s", ctx.Request.RawRequest())
// next handler
ctx.Next()
// print Response
logs.Infof("Send RawResponse: %s", ctx.Reponse.RawResponse())
})
h.POST("/login", func(c context.Context, ctx *app.RequestContext){
// some biz logic
ctx.JSON(200, "OK")
})
h.POST("/logout", func(c context.Context, ctx *app.RequestContext){
// some biz logic
ctx.JSON(200, "OK")
})
}
如何实现注册多个中间件呢
那就是对于同一个处理逻辑,将所有中间件之间通过Next()方法和index下标来实现中间件的链式传递
对于中止转递的方法有
- 调整index到最大值,循环自动退出
- 调用
abort()函数
路由设计
框架路由实际上就是为URL匹配对应的处理函数(Handlers)
- 静态路由:/a/b/c、/a/b/d
- 参数路由:/a/:id/c (/a/b/c, /a/d/c)、/*all
- 路由修复:/a/b/ <-> /a/b
- 冲突路由以及优先级
- 匹配HTTP的方法
- 多处理函数:方便添加中间件
实例可以参考后续介绍到的Gin框架的路由树
协议层设计
抽象出合适的接口就行,将具体的七层网络结构给隐藏
网络层设计
待填坑
Gin结构解析
gin.H
gin.H是map[string]interface{}的一个快捷名称,写起来更简洁
type H map[string]interface{}
gin.Engine
Engine是Gin框架最重要的数据结构,它是框架的入口。我们通过Engine对象来定义服务路由信息,组装插件,运行服务。Engine如中文意思一致,它就是框架的核心发动机,整个web服务都是由它来驱动的。
Engine 对象很简单,因为引擎最重要的部分 —— 底层的 HTTP 服务器使用的是 Go 语言内置的 http server,Engine 的本质只是对内置的 HTTP 服务器的包装,让它使用起来更加便捷。
gin.Default()函数会生成一个默认的Engine对象,里面包含了两个默认的常用插件,分别是Logger和Recovery,Logger用于输出请求日志,Recovery确保单个请求发送panic时记录异常堆栈日志,输出统一的错误相应
func Default() *Engine {
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
路由树
在Gin框架中,路由规则被分成了最多9棵前缀树,每一个HTTP Method对应一棵[前缀树]
HTTP协议中的四个Method
GET:用来获取资源
POST:用来新建资源
PUT:用来更新资源
DELETE:用来删除资源
树的节点按照URL中的/符号进行层级划分,URL支持:name形式的名称匹配,还支持*subpath形式的路径匹配符
每个节点都会挂接若干请求函数构成一个请求函数链HandlersChain,当一个请求到来时,在其Method对应的前缀树上找到对应URL的节点,拿到对应的请求函数链来执行就完成了请求的过程
type Engine struct{
...
trees methodTrees
...
}
type methodTrees []methodTree
type methodTree struct{
method string
root *node //树根
}
type node struct {
path string // 当前节点的路径
...
handlers HandlersChain // 请求处理链
}
type HandlersChain []HandlerFunc
type HandlerFunc func(*context)
Engine 对象包含一个addRoute方法用于添加URL请求处理器,它会将对应的路径和处理器挂接到相应的请求树中
func (e *Engine) addRoute(mothod, path string, handlers HandlersChain)
gin.RouteGroup
RouterGroup是对路由树的包装,所有的路由规划最终都是由它来管理。Engine结构体继承了RouterGroup,所以Engine直接具备了RouterGroup所有对路由功能的管理,也就是为什么在hello world的例子可以直接使用Engine对象来定义路由规则
r := gin.Default()
r.Get(string,...HandlerFunc)
...
同时RouterGroup对象里也有Engine的指针,这样Engine和RouterGroup就成了[你中有我,我中有你]的关系
type Engine struct {
RouterGroup
...
}
type RouterGroup struct {
...
engine *Engine
...
}
RouterGroup实现了IRoute接口,暴露了一系列路由方法,这些方法最终都是通过调用Engine.addRoute方法将请求处理器挂接到路由树中。
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
// 匹配所有的HTTP Method
ANY(string, ...HandlerFunc) IRoutes
RouterGroup内部有一个前缀路径属性,它会将所有的子路径都加上这个前缀再放进路由树中。有了这个前缀路径,就可以实现URL分组功能。
Engine对象内嵌的RouterGroup对象的前缀路径是/,表示根路径,RouerGroup支持分组嵌套,使用Group方法就可以让分组下面再挂分组
func main() {
// return one root Engine / root RouterGroup
router := gin.Default()
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEngpoint)
}
v1 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}
//default is 8080
router.Run(":8080")
}
上面的例子实际已经使用了分组嵌套,因为Engine对象里的RouterGroup对象就是第一层分组(root node),v1和v2都是根分组的子分组(root node的child node)
gin.Context
这个对象里保存了请求的上下文信息,它是所有请求处理器的入口参数
type HandlerFunc func(*Context)
type Context struct {
...
Request *http.Request // 请求对象
Writer ResponseWriter // 相应对象
Params Params // URL匹配参数
...
Keys map[string]interface{} //自定义上下文信息
}
Context对象提供了分成丰富的方法用于获取当前请求的上下文信息,如果你需要获取请求的URL参数、Cookie、Header都可以通过Context对象获取,这一系列方法本质上是对http.Request对象的封装
// 获取 URL 匹配参数 /book/:id
func (c *Context) Param(key string) string
// 获取 URL 查询参数 /book?id=123&page=10
func (c *Context) Query(key string) string
// 获取 POST 表单参数
func (c *Context) PostForm(key string) string
// 获取上传的文件对象
func (c *Context) FormFile(name string) (*multipart.FileHeader, error)
// 获取请求Cookie
func (c *Context) Cookie(name string) (string, error)
...
Context 对象提供了很多内置的响应形式,JSON、HTML、Protobuf 、MsgPack、Yaml 等。它会为每一种形式都单独定制一个渲染器。通常这些内置渲染器已经足够应付绝大多数场景,如果你觉得不够,还可以自定义渲染器。
func (c *Context) JSON(code int, obj interface{})
func (c *Context) Protobuf(code int, obj interface{})
func (c *Context) YAML(code int, obj interface{})
...
// 自定义渲染
func (c *Context) Render(code int, r render.Render)
// 渲染器通用接口
type Render interface {
Render(http.ResponseWriter) error
WriteContentType(w http.ResponseWriter)
}
所有的渲染器最终还是需要调用内置的http.ResponseWriter(Contetx.Writer)将相应对象转换成字节流写到套接字中
type ResponseWriter interface {
// 容纳所有的响应头
Header() Header
// 写Body
Write([byte]) (int, error)
// 写Header
WriteHeader (statusCode, int)
}
插件和请求链
我们编写业务代码时一般也就是一个处理函数,为什么路由节点需要挂接一个函数链呢?
type node struct {
path string //当前节点的路径
...
handlers handlerChain //请求处理链
...
}
type HandlerChain []HandlerFunc
type HandlerFunc func(*Context)
这是因为Gin提供了插件,只有函数链尾部是业务处理,前面的部分都是插件函数。在Gin中插件和业务处理函数形式都是一样的,都是func(*Context)。当我们定义路由时,Gin会将插件和业务处理函数合并在一起形成一个链条结构
type Context struct {
...
index uint8 //当前的业务逻辑位于函数链的位置
handlers HandlersChain // 函数链
...
}
// 挨个调用链条中的处理函数
func (c *Context) Next() {
c.index ++
for s:=int8(len(c.handlers)); c.index<s; c.index++ {
c.handlers[c.index](c)
}
}
Gin在接收到客户端请求后,找到相应的处理链,构造一个Context对象,再调用它的Next()方法就正式进入了请求处理的全流程
Gin还支持Abort()方法中断请求链的执行,它的原理时将Condext.Index调整到一个比较大的数字,这样Next()方法中的调用循环就会立马结束。需要注意的Abort()方法并不是通过panic的方式中断执行流,执行Abort方法后,当前函数内后面的代码逻辑还会继续执行
const abortIndex = 127
func (c *Context) Abort() {
c.index = abortIndex
}
func somePlugin(c *Context) {
...
if condition {
c.Abort()
// continue executing
}
}
RouterGroup提供了Use()方法来注册插件,因为RouterGroup是一层套一层,不同层级的路由可能会注册不一样的插件,最终不同的路由节点挂接的处理函数链也不尽相同
RouterGroup实际上就是一个节点,就是一个相对根目录,为什么又说是Group因为这个目录下也可以有子目录(子树),它也可以有父目录(父节点)。一个节点就有一条对应的函数链。
//注册插件plugin
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middlerware...)
return group.returnObj()
}
//注册Get请求-找到GET的路由树
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("GET",relativePath, handlers)
}
func (group *RouterGroup) handle(httpmethod, relativepath string, handlers HandlersFChain) IRoutes {
//合并URL(RouterGroup有URL前缀)
absolutePath := group.calculateAbsolutePath(relativepath)
// 合并处理链条
handlers = group.combineHandlers(handlers)
// 注册路由树
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj
}
HTTP错误
当URL请求的路径不能在路由树里找到时,就需要处理404 NOTFound错误。当URL的请求路径可以在路由树里找到,但是Method不匹配,就需要处理405 MethodNotAllowed错误。Engine对象为这两个错误提供了处理器注册的入口。
func (e *Engine) NoMethod(handlers ...HandlersFunc)
func (e *Engine) NoRoute(handlers ...HandlersFunc)
异常处理器和普通处理器一样,也要和插件函数组合在一起形成一个调用链,如果没有提供异常处理器,Gin就会使用内置的简单错误处理器
注意这两个处理器是定义在Engine的全局对象上,而不是RouterGroup,Engine就是根目录对应的RouterGroup,其他的子节点RouterGroup就不代表Engine了。对于非404和405错误,需要用户子自定义插件来处理,对于panic跑出来的异常也需要自定义plugin来处理