前言
我们先用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呢?
其实框架的好处,就是别人帮我们搭建了一个舞台,同时提供了很多现成的轮子,让我们专注于业务的开发,同时让开发效率更高。
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来处理