Golang架构直通车——深入理解Gin骨架

Gin的初始化流程

package main

import "github.com/gin-gonic/gin"

func main() {
        r := gin.Default()
        r.GET("/ping", func(c *gin.Context) {
                c.JSON(200, gin.H{
                        "message": "pong",
                })
        })
        r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

首先进行初始化 gin.Default(), 接着定义了一个叫做 /ping 的路由, 最后直接启动了 r.Run()。简单可以总结为下面几个过程:

  1. ENGINE初始化
  2. 注册handler
  3. 运行和接受请求

ENGINE初始化

首先, 查看下 gin.Default() 的过程:

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
        debugPrintWARNINGDefault()
        engine := New()
        engine.Use(Logger(), Recovery())
        return engine
}

Default 的主要功能是初始化 Engine, 然后加载了两个中间件, 用于日志记录和恢复。Engine 的初始化有两种方式, Default() 和 New(),实际上Default()在内部也调用了New()方法。

再看一下, 添加中间件的过程。

// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
        engine.RouterGroup.Use(middleware...)
        engine.rebuild404Handlers()
        engine.rebuild405Handlers()
        return engine
}

添加中间件, 实际上是在 RouterGroup 上做注册,其内部包含路由路径和中间件数组handlers。所以添加中间件只是在 Handlers 中新加一个元素。

// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
        group.Handlers = append(group.Handlers, middleware...)
        return group.returnObj()
}

func (group *RouterGroup) returnObj() IRoutes {
        if group.root {
                return group.engine
        }
        return group
}

注册handler

web 服务器最主要的当然是定义路由和处理函数了。
Engine 的内部使用了 RouterGroup, 所以其实上各种 HTTP 方法都是注册在 RouterGroup 上的. 具体看一下 GET 方法:

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
        return group.handle("GET", relativePath, handlers)
}

通过注释和代码, 我们可以知道, GET 只是一个快捷方式, 其实所有的 HTTP 方法注册都是由 router.Handle 处理的:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
        absolutePath := group.calculateAbsolutePath(relativePath)
        handlers = group.combineHandlers(handlers)
        group.engine.addRoute(httpMethod, absolutePath, handlers)
        return group.returnObj()
}

handle 的核心语句是 group.engine.addRoute(httpMethod, absolutePath, handlers)。这一点我们之后再详细解释,总之, 到这里, 路由已经注册好了。

运行和接受请求

r.Run()源码如下:

// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
        defer func() { debugPrintError(err) }()

        address := resolveAddress(addr)
        debugPrint("Listening and serving HTTP on %s\n", address)
        err = http.ListenAndServe(address, engine)
        return
}

这是一个阻塞的方法。内部使用 net/http 包的 ListenAndServe 函数:
func ListenAndServe(addr string, handler Handler) error
第二个参数的类型是 Handler, 一猜就知道应该是接口类型, 看一下具体要实现什么:

type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
        c := engine.pool.Get().(*Context)
        c.writermem.reset(w)
        c.Request = req
        c.reset()

        engine.handleHTTPRequest(c)

        engine.pool.Put(c)
}

engine.handleHTTPRequest©, 这用于处理 HTTP 请求:主要根据 HTTP 方法和路径从 engine.trees 找到 对应的 handlers。handlers的运行借助于Next(),可以将 c.Next() 理解为控制流转移, 每当运行 c.Next(), 实际上是运行下一个 handler. 有点类似递归时的调用栈。

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
        c.index++
        for c.index < int8(len(c.handlers)) {
                c.handlers[c.index](c)
                c.index++
        }
}

Gin框架的核心结构

我们都知道开发一个HTTP服务,首先需要启动一个TCP监听,然后需要有一些列的handler来处理具体的业务逻辑,最后在再将具体的业务逻辑通过HTTP协议约定和相关的Method和URL进行绑定,以此来对外提供具体功能的HTTP服务。

在Gin框架与之对应的就是如下几个模型:

  • Engine: 用来初始化一个gin对象实例,在该对象实例中主要包含了一些框架的基础功能,比如日志,中间件设置,路由控制(组),以及handlercontext等相关方法.源码文件
  • Router: 用来定义各种路由规则和条件,并通过HTTP服务将具体的路由注册到一个由context实现的handler中
  • Context: Context是框架中非常重要的一点,它允许我们在中间件间共享变量,管理整个流程,验证请求的json以及提供一个json的响应体. 通常情况下我们的业务逻辑处理也是在整个Context引用对象中进行实现的.
  • Bind: 在Context中我们已经可以获取到请求的详细信息,比如HTTP请求头和请求体,但是我们需要根据不同的HTTP协议参数来获取相应的格式化数据来处理底层的业务逻辑,就需要使用Bind相关的结构方法来解析context中的HTTP数据

ENGINE

Engine结构体不全部展示了,这里挑几个具备代表性的属性做说明:

type Engine struct {
    ...

    // 路由组,在实际开发过程中我们通常会使用路由组来组织和管理一些列的路由. 比如: /apis/,/v1/等分组路由
    RouterGroup
    // methodTrees是methodTree的切片(methodTree是一个包含请求方法和node指针的结构体,node是一个管理path的节点树)
    trees            methodTrees

    ...
}

初始化Engine的方式:

  • New(): 该函数返回一个默认的Engine引用实例(开启了自动重定向,转发客户端ip和禁止请求路径转义)
  • Default(): 内部调用New()函数,但是增加了Logger和Recovery两个中间件

1.1 RouterGroup
Gin 的 Engine 结构体内嵌了 RouterGroup 结构体,定义了 GET,POST 等路由注册方法。


1.2 methodTrees

Engine 中的 trees 字段定义了路由逻辑。trees 是 methodTrees 类型(其实就是 []methodTree数组,初始化语句是trees: make(methodTrees, 0, 9)),不同请求方法的路由在不同的树(methodTree)中。接下来详细说明。

Gin添加路由主要是由 addRoute 完成:

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
        //三个断言,判断path格式、method不为空和处理器handler数目至少为1
        assert1(path[0] == '/', "path must begin with '/'")
        assert1(method != "", "HTTP method can not be empty")
        assert1(len(handlers) > 0, "there must be at least one handler")
        
        debugPrintRoute(method, path, handlers)
        //根据method找到对应的methodTree
        root := engine.trees.get(method)
        //如果methodTree不存在,那么就创建一个methodTree
        if root == nil {
                root = new(node)
                root.fullPath = "/"
                engine.trees = append(engine.trees, methodTree{method: method, root: root})
        }
        //添加路由
        root.addRoute(path, handlers)
}

而methodTree的结构如下:

type methodTree struct {
        method string
        root   *node
}
type methodTrees []methodTree

前面添加路由的代码中第一步是找到 root, 即 root := engine.trees.get(method), 结合 get 代码, 我们可以发现 methodTrees 实际上根据 HTTP 方法分类的, 每种方法都对应一颗树:

func (trees methodTrees) get(method string) *node {
        for _, tree := range trees {
                if tree.method == method {
                        return tree.root
                }
        }
        return nil
}

下一步if root == nil ,如果get出来的root不存在,那么就就新建一棵树 methodTree。

再看一下树的节点是如何定义的:

type nodeType uint8

const (
        static nodeType = iota // default
        root
        param
        catchAll
)

type node struct {
        path      string
        indices   string
        children  []*node
        handlers  HandlersChain
        priority  uint32
        nType     nodeType
        maxParams uint8
        wildChild bool
        fullPath  string
}

数据结构已经了解了, 看一下路由到底是如何添加的, 即 root.addRoute(path, handlers)

// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
func (n *node) addRoute(path string, handlers HandlersChain) {
        fullPath := path
        n.priority++
        numParams := countParams(path)

        parentFullPathIndex := 0

        // non-empty tree
        if len(n.path) > 0 || len(n.children) > 0 {
           ...代码太长了,省略
        } else { // Empty tree
                n.insertChild(numParams, path, fullPath, handlers)
                n.nType = root
        }
}

代码有些长,一步一步分析:
先根据 if 语句分为两种情况if len(n.path) > 0 || len(n.children) > 0

  • 一种是初始化的时候(即树是空的):树是空的情况下, 即 n.path 是空字符串(初始值) 且 n.children 是空切片. 这个时候, 只是通过 insertChild 插入节点, 然后将节点的类型设置为 root
  • 另一种是树是非空的。

当树是非空的,使用算法的就是Radix 树, 是Trie树的紧凑版变种,其中作为唯一子节点的每个节点都与其父节点合并。比如, 当前节点 node.path = “/ping”, 遇到 path = “/pong” 时就会分裂, 公共前缀的长度 i=2, 因此节点会分裂为 node.path = “/p” 和 node.path = “ing”. 分裂出来的后一个节点会占据当前节点的大部分属性.

在构建Radix路径的时候,会做节点的“公共前缀”分裂,然后 对新加入的节点做handlers的绑定。

// insert remaining path part and handle to the leaf
n.path = path[offset:]
n.handlers = handlers
n.fullPath = fullPath

ROUTER

使用Engine结构体中提供的相关方法,我们就可以快速的启动一个HTTP服务了,但是如何对外暴露一个URL来简单实现一个HTTP的数据传输呢,这个时候就需要使用Router中的方法了。

Gin框架中Router相关的结构体:

  • RouterGroup: 该结构体被用来在Gin内部配置一个路由,一个RouterGroup被用来关联URL前缀和一组具体的handler业务逻辑
  • IRoutes: IRoutes是一个定了了所有路由处理的接口(包含一些常用的HTTP方法)
  • IRouter: IRouter则是一个包含单个路由和路由组的所有路由处理的接口
// RouterGroup 结构体
type RouterGroup struct {
        Handlers HandlersChain
        basePath string
        engine   *Engine
        root     bool
}

// IRoutes 接口
type IRoutes interface {
        Use(...HandlerFunc) IRoutes

        Handle(string, string, ...HandlerFunc) IRoutes
        Any(string, ...HandlerFunc) IRoutes
        GET(string, ...HandlerFunc) IRoutes
        POST(string, ...HandlerFunc) IRoutes
        DELETE(string, ...HandlerFunc) IRoutes
        PATCH(string, ...HandlerFunc) IRoutes
        PUT(string, ...HandlerFunc) IRoutes
        OPTIONS(string, ...HandlerFunc) IRoutes
        HEAD(string, ...HandlerFunc) IRoutes

        StaticFile(string, string) IRoutes
        Static(string, string) IRoutes
        StaticFS(string, http.FileSystem) IRoutes
}

// IRouter接口
type IRouter interface {
        IRoutes
        Group(string, ...HandlerFunc) *RouterGroup
}

CONTEXT

在Gin框架中由Router结构体来负责路由和方法(URL和HTTP方法)的绑定,Router内的Handler采用Context结构体来处理具体的HTTP数据传输方式,比如HTTP头部,请求体参数,状态码以及响应体和其他的一些常见HTTP行为。

type Context struct {
    // 一个包含size,status和ResponseWriter的结构体
    writermem responseWriter
    // http的请求体(指向原生的http.Request指针)
    Request   *http.Request
    // ResonseWriter接口
    Writer    ResponseWriter

    // 请求参数[]{"Key":"Value"}
    Params   Params
    handlers HandlersChain
    index int8
    // http请求的全路径地址
    fullPath string
    // gin框架的Engine结构体指针
    engine   *Engine
    // 每个请求的context中的唯一键值对
    Keys map[string]interface{}
    // 绑定到所有使用该context的handler/middlewares的错误列表
    Errors errorMsgs
    // 定义了允许的格式被用于内容协商(content)
    Accepted []string
    // queryCache 使用url.ParseQuery来缓存参数查询结果(c.Request.URL.Query())
    queryCache url.Values
    // formCache 使用url.ParseQuery来缓存PostForm包含的表单数据(来自POST,PATCH,PUT请求体参数)
    formCache url.Values
}

Context初始化

Context 是在每次接受请求的时候初始化的:

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
        c := engine.pool.Get().(*Context)
        c.writermem.reset(w)
        c.Request = req
        c.reset()

        engine.handleHTTPRequest(c)

        engine.pool.Put(c)
}

里面用到了 sync.Pool, sync.Pool 适用于缓存已分配但未使用的 items, 以便后续重用, 并减轻垃圾回收的压力。

Context获取请求参数

接着看一下如何获取请求参数, 比如 URL 中的参数, GET 中的 query, 或者是 POST 中的 data。

func (c *Context) Param(key string) string {
        return c.Params.ByName(key)
}

func (c *Context) Query(key string) string {
        value, _ := c.GetQuery(key)
        return value
}

... 只展示几个

Context 之模型绑定和验证

模型绑定是一个非常有用的能力, 尤其是和验证结合在一起. 处理请求参数时, 一大重点就是验证.
Gin 支持两种类型的绑定, Must bind 和 Should bind. 请求类型则支持 JSON, XML, YAML 和标准表单绑定.

// Bind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
//     "application/json" --> JSON binding
//     "application/xml"  --> XML binding
// otherwise --> returns an error.
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer.
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
func (c *Context) Bind(obj interface{}) error {
        b := binding.Default(c.Request.Method, c.ContentType())
        return c.MustBindWith(obj, b)
}

// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).
func (c *Context) BindJSON(obj interface{}) error {
        return c.MustBindWith(obj, binding.JSON)
}

// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
func (c *Context) BindXML(obj interface{}) error {
        return c.MustBindWith(obj, binding.XML)
}

// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
func (c *Context) BindYAML(obj interface{}) error {
        return c.MustBindWith(obj, binding.YAML)
}

....只展示几个

从上面的代码可以发现, MustBindWith 其实是 ShouldBindWith 的包装, 具体内容还是要看 ShouldBindWith。但实际上 ShouldBindWith 也只是调用了 binding.Binding 上的方法而言:

// ShouldBindWith binds the passed struct pointer using the specified binding engine.
// See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
        return b.Bind(c.Request, obj)
}

Bingding :
// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {
        Name() string
        Bind(*http.Request, interface{}) error
}

// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
        Binding
        BindBody([]byte, interface{}) error
}

// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params.
type BindingUri interface {
        Name() string
        BindUri(map[string][]string, interface{}) error
}

// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (
        JSON          = jsonBinding{}
        XML           = xmlBinding{}
        Form          = formBinding{}
        Query         = queryBinding{}
        FormPost      = formPostBinding{}
        FormMultipart = formMultipartBinding{}
        ProtoBuf      = protobufBinding{}
        MsgPack       = msgpackBinding{}
        YAML          = yamlBinding{}
        Uri           = uriBinding{}
        Header        = headerBinding{}
)

以 JSON 为例,jsonBinding实际上是实现了json的编码和解码而已
解码的最后一步是验证, 调用了 validate 函数:

func validate(obj interface{}) error {
        if Validator == nil {
                return nil
        }
        return Validator.ValidateStruct(obj)
}

Context 之响应

看完了请求参数的获取和模型绑定之后, 来看看响应是如何发送的。先来看一下 Context 中用到的 responseWriter 类型和 ResponseWriter 类型:

type Context struct {
        writermem responseWriter
        Request   *http.Request
        Writer    ResponseWriter
...
}

ResponseWriter 接口组合了 http 包中用于响应的数据结构, 所有的方法上都有注释. 而 responseWriter 实际上就是实现了 ResponseWriter 接口的结构体。
Writer 是用于写入响应的, 而从 writermem 名字的后缀, 可以推断出这和内存有关。

内容协商通过 Accept Header 实现, 用于为不同类型的客户端提供不同类型的资源, 比如协商网页语言或响应格式等。在此不做赘述了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值