Gin框架结构分析

前言

我们先用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页面了

image-20200913203807251

我们可以给文字添加色彩

// http.ResponseWriter:代表响应,传递到前端的
// *http.Request:表示请求,从前端传递过来的
func handler(w http.ResponseWriter, r *http.Request) {
    str := "<h1 style='color:red'>hello Golang!<h1>"
	fmt.Fprintln(w, str);
}

然后重启后,在刷新

image-20200913203922973

我们还可以把里面的字符串放在一个文件里,我们定义一个 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.Hmap[string]interface{}的一个快捷名称,写起来更简洁

type H map[string]interface{}

gin.Engine

Engine是Gin框架最重要的数据结构,它是框架的入口。我们通过Engine对象来定义服务路由信息,组装插件,运行服务。Engine如中文意思一致,它就是框架的核心发动机,整个web服务都是由它来驱动的。

Engine 对象很简单,因为引擎最重要的部分 —— 底层的 HTTP 服务器使用的是 Go 语言内置的 http server,Engine 的本质只是对内置的 HTTP 服务器的包装,让它使用起来更加便捷。

gin.Default()函数会生成一个默认的Engine对象,里面包含了两个默认的常用插件,分别是LoggerRecoveryLogger用于输出请求日志,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形式的路径匹配符

img

每个节点都会挂接若干请求函数构成一个请求函数链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()方法就正式进入了请求处理的全流程

img

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来处理

参考博客

轻量级 Web 框架 Gin 结构分析
Gin内容介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

linengcs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值