gin框架总结

voidFan · · 2881 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

gin框架总结

一 gin框架初识

1.1 helloworld

gin框架中的路由是基于httprouter开发的。HelloWorld:

package main
import (
    "github.com/gin-gonic/gin"
    "fmt"
)
func main() {
    r := gin.Default()    //Default返回一个默认路由引擎
    r.GET("/", func(c *gin.Context) {
        username := c.Query("username")
        fmt.Println(username)
        c.JSON(200, gin.H{
            "msg":"hello world",
        })
    })
    r.Run()            //默认位于0.0.0.0:8080,可传入参数":3030";也可以绑定服务器
}

二 参数获取

2.1 get请求参数

常见参数获取方式:

c.Query("username")
c.QueryDefault("username","lisi")       //如果username为空,则赋值为lisi

路由地址为:/user/:name/:pass,获取参数:

name := c.Param("name")

2.2 post请求参数获取

name := c.PostForm("name")

2.3 参数绑定

参数绑定利用反射机制,自动提取querystring,form表单,json,xml等参数到结构体中,可以极大提升开发效率。

package main
import (
    "net/http"
    "github.com/gin-gonic/gin"
    "fmt"
)
type User struct {
    Username string `form:"username" json:"username" binding:"required"`
    Password string `form:"password" json:"password" binding:"required"`
}

func login(c *gin.Context) {
    var user User
    fmt.Println(c.PostForm("username"))
    fmt.Println(c.PostForm("password"))
    err := c.ShouldBind(&user)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error()
        })
    }

    c.JSON(http.StatusOK, gin.H{
        "username": user.Username,
        "password": user.Password,
    })
}


func main() {
    router := gin.Default()
    router.POST("/login", login)
    router.Run(":3000")
}

三 静态文件

静态化当前目录下static文件夹:

    router := gin.Default()
    router.Static("/static", "./static")
    router.Run(":3000")

注意:同样推荐使用go build,不要使用开发工具的run功能。

四 结果返回

4.1 返回JSON

c.JSON(200,gin.H{"msg":"OK"})
c.JSON(200,结构体)

4.2 返回模板

    router.LoadHTMLGlob("templates/**/*")
    router.GET("/test/index", func(c *gin.Context){
        c.HTML(http.StatusOK, "test/index.tmpl", gin.H{
            "msg": "test",
        })
    })

模板文件:index.tmpl


{{define "test/index.tmpl"}}
<html>

    <head>
    </head>

    <body>

        test...

        {{.}}
        -----
        {{.msg}}

    </body>

</html>

{{end}}

注意事项:不要使用编辑器的run功能,会出现路径错误,推荐使用命令build,项目路径分配如下:


gin-01.png

五 文件上传

5.1 单文件上传

 router.POST("/upload", func (c *gin.Context) {
    file, err := c.FormFile("file")
    if (err != nil) {
        c.JSON(http.StatusInternalServerError, gin.H{
            "msg": err.Error(),
        })
        return
    }
    dst := fmt.Sprintf("/uploads/&s", file.Filename)
    c.SavaeUpLoadedFile(file, dst)
    c.JSON(http.StatusOK, gin.H{
        "msg":"ok",
    })
 })

5.2 多文件上传

 router.POST("/upload", func(c *gin.Context) {
        // 多文件
        form, _ := c.MultipartForm()
        files := form.File["upload[]"]

        for _, file := range files {
            log.Println(file.Filename)

            // 上传文件到指定的路径
            // c.SaveUploadedFile(file, dst)
        }
        c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
    })

一 路由分组

访问路径是:/user/login/user/signin

package main

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

func login(c *gin.Context) {
    c.JSON(300, gin.H{
        "msg": "login",
    })
}

func logout(c *gin.Context) {
    c.JSON(300, gin.H{
        "msg": "logout",
    })
}

func main() {

    router := gin.Default()

    user := router.Group("/user")
    {
        user.GET("/login", login)
        user.GET("/logout", logout)
    }

    router.Run(":3000")
}

二 路由设计

2.0 项目结构

笔者自己的路由设计,仅供参考:

项目结构如图:

gin-02.png

2.1 main.go

main.go:

package main

import (
    "Demo1/router"
)

func main() {
    r := router.InitRouter()
    _ = r.Run()
}

2.2 路由模块化核心 routes.go

routes.go:

package router

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

func InitRouter() *gin.Engine {

    r := gin.Default()

    // 路由模块化
    userRouter(r)
    orderRouter(r)

    return r
}

2.3 业务处理

userRouter.go示例:

package router

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func userRouter(r *gin.Engine) {

    r.GET("/user/login", userLogin)

}

func userLogin(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "code": 10001,
        "msg": "登录成功",
        "data": nil,
    })
}

gin配合单元测试

https://github.com/stretchr/testify/assert 是个很好的单元测试框架。

在上一节中配置了笔者自己项目的路由模块化思路,下面是配套的单元测试demo:

userRouter_test.go

package test

import (
    "Demo1/router"
    "github.com/stretchr/testify/assert"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestUserRouter_userLogin(t *testing.T) {
    r := router.InitRouter()
    w := httptest.NewRecorder()
    req, _ := http.NewRequest(http.MethodGet, "/user/login", nil)
    r.ServeHTTP(w, req)
    assert.Equal(t, http.StatusOK, w.Code)
    assert.Equal(t, `{"code":10001,"data":null,"msg":"登录成功"}`, w.Body.String())
}

一 Gin中间件

1.1 中间件的概念

gin框架允许在处理请求时,加入用户自己的钩子函数,该钩子函数即中间件。他的作用与Java中的拦截器,Node中的中间件相似。

中间件需要返回gin.HandlerFunc函数,多个中间件通过Next函数来依次执行。

1.2 入门使用案例

现在设计一个中间件,在每次路由函数执行前打印一句话,在上一节的项目基础上新建middleware文件夹,新建一个中间件文件MyFmt.go

package middleware

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

// 定义一个中间件
func MyFMT() gin.HandlerFunc {
    return func(c *gin.Context) {
        host := c.Request.Host
        fmt.Printf("Before: %s\n",host)
        c.Next()
        fmt.Println("Next: ...")
    }
}

在路由函数中使用中间件:

r.GET("/user/login", middleware.MyFMT(),  userLogin)

打印结果:

Before: localhost:8080
Next: ...
[GIN] 2019/07/28 - 16:28:16 | 200 |      266.33µs |             ::1 | GET      /user/login

1.2 中间件的详细使用方式

全局中间件:直接使用 gin.Engine结构体的Use()方法,中间件将会在项目的全局起作用。

func InitRouter() *gin.Engine {

    r := gin.Default()

    // 全局中间件
    r.Use(middleware.MyFMT())

    // 路由模块化
    userRouter(r)
    orderRouter(r)

    return r
}

路由分钟中使用中间件:

router := gin.New()
user := router.Group("user", gin.Logger(),gin.Recovery())
{
    user.GET("info", func(context *gin.Context) {

    })
    user.GET("article", func(context *gin.Context) {

    })
}

单个路由使用中间件(支持多个中间件的使用):

router := gin.New()
router.GET("/test",gin.Recovery(),gin.Logger(),func(c *gin.Context){
    c.JSON(200,"test")
})

1.3 内置中间件

Gin也内置了一些中间件,可以直接使用:

func BasicAuth(accounts Accounts) HandlerFunc
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc
func Bind(val interface{}) HandlerFunc //拦截请求参数并进行绑定
func ErrorLogger() HandlerFunc       //错误日志处理
func ErrorLoggerT(typ ErrorType) HandlerFunc //自定义类型的错误日志处理
func Logger() HandlerFunc //日志记录
func LoggerWithConfig(conf LoggerConfig) HandlerFunc
func LoggerWithFormatter(f LogFormatter) HandlerFunc
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc
func Recovery() HandlerFunc
func RecoveryWithWriter(out io.Writer) HandlerFunc
func WrapF(f http.HandlerFunc) HandlerFunc //将http.HandlerFunc包装成中间件
func WrapH(h http.Handler) HandlerFunc //将http.Handler包装成中间件

二 请求的拦截与后置

中间件的最大作用就是拦截过滤请求,比如我们有些请求需要用户登录或者需要特定权限才能访问,这时候便可以中间件中做过滤拦截。

下面三个方法中断请求后,直接返回200,但响应的body中不会有数据:

func (c *Context) Abort()
func (c *Context) AbortWithError(code int, err error) *Error
func (c *Context) AbortWithStatus(code int)
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{})        // 中断后可以返回json数据

如果在中间件中调用gin.Context的Next()方法,则可以请求到达并完成业务处理后,再经过中间件后置拦截处理:

func MyMiddleware(c *gin.Context){
    //请求前
    c.Next()
    //请求后
}

一 gin.Engine

Engine是框架的入口,是gin框架的核心,通过Engine对象来定义服务路由信息、组装插件、运行服务。不过Engine的本质只是对内置HTTP服务的包装。

gin.Default() 函数会生成一个默认的 Engine 对象,包含2个默认常用插件

  • Logger:用于输出请求日志
  • Recovery:用于确保单个请求发生 panic 时记录异常堆栈日志,输出统一的错误响应。
func Default() *Engine {
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

二 gin的路由

2.1 路由树

在 Gin 框架中,路由规则被分成了最多 9 棵前缀树,每一个 HTTP Method对应一棵 前缀树 ,树的节点按照 URL 中的 / 符号进行层级划分,URL 支持 :name 形式的名称匹配,还支持 *subpath 形式的路径通配符:

// 匹配单节点 named
pattern = /book/:id
match /book/123
nomatch /book/123/10
nomatch /book/

// 匹配子节点 catchAll mode
/book/*subpath
match /book/
match /book/123
match /book/123/10

如图所示:

gin-03.jpeg

每个节点都会挂接若干请求处理函数构成一个请求处理链 HandlersChain。当一个请求到来时,在这棵树上找到请求 URL 对应的节点,拿到对应的请求处理链来执行就完成了请求的处理。

type Engine struct {
  ...
  trees methodTrees
  ...
}

type methodTrees []methodTree

type methodTree struct {
    method string
    root   *node  // 树根
}

type node struct {
  path string // 当前节点的路径
  ...
  handlers HandlersChain // 请求处理链
  ...
}

type HandlerFunc func(*Context)

type HandlersChain []HandlerFunc

Engine 对象包含一个 addRoute 方法用于添加 URL 请求处理器,它会将对应的路径和处理器挂接到相应的请求树中:

func (e *Engine) addRoute(method, path string, handlers HandlersChain)

2.2 路由组

RouterGroup 是对路由树的包装,所有的路由规则最终都是由它来进行管理。Engine 结构体继承了 RouterGroup ,所以 Engine 直接具备了 RouterGroup 所有的路由管理功能,同时 RouteGroup 对象里面还会包含一个 Engine 的指针,这样 Engine 和 RouteGroup 就成了「你中有我我中有你」的关系。

type Engine struct {
  RouterGroup
  ...
}

type RouterGroup struct {
  ...
  engine *Engine
  ...
}

RouterGroup 实现了 IRouter 接口,暴露了一系列路由方法,这些方法最终都是通过调用 Engine.addRoute 方法将请求处理器挂接到路由树中。

  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
  // 匹配所有 HTTP Method
  Any(string, ...HandlerFunc) IRoutes

RouterGroup 内部有一个前缀路径属性,它会将所有的子路径都加上这个前缀再放进路由树中。有了这个前缀路径,就可以实现 URL 分组功能。
Engine 对象内嵌的 RouterGroup 对象的前缀路径是 /,它表示根路径。RouterGroup 支持分组嵌套,使用 Group 方法就可以让分组下面再挂分组,依次类推。

2.3 HTTP错误

当 URL 请求对应的路径不能在路由树里找到时,就需要处理 404 NotFound 错误。当 URL 的请求路径可以在路由树里找到,但是 Method 不匹配,就需要处理 405 MethodNotAllowed 错误。Engine 对象为这两个错误提供了处理器注册的入口。

func (engine *Engine) NoMethod(handlers ...HandlerFunc)
func (engine *Engine) NoRoute(handlers ...HandlerFunc)

异常处理器和普通处理器一样,也需要和插件函数组合在一起形成一个调用链。如果没有提供异常处理器,Gin 就会使用内置的简易错误处理器。

注意这两个错误处理器是定义在 Engine 全局对象上,而不是 RouterGroup。对于非 404 和 405 错误,需要用户自定义插件来处理。对于 panic 抛出来的异常需要也需要使用插件来处理。

2.4 HTTPS

Gin 不支持 HTTPS,官方建议是使用 Nginx 来转发 HTTPS 请求到 Gin。

三 gin.Context

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(Context.Writer) 将响应对象转换成字节流写到套接字中。

type ResponseWriter interface {
 // 容纳所有的响应头
 Header() Header
 // 写Body
 Write([]byte) (int, error)
 // 写Header
 WriteHeader(statusCode int)
}

四 插件与请求链

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-04.jpeg

一 请求流程梳理

首先从gin最开始的创建engine对象部分开始:

router := gin.Default()

该方法返回了Engine结构体,常见属性有:

type Engine struct {
    //路由组
    RouterGroup
    RedirectTrailingSlash bool
    RedirectFixedPath bool
    HandleMethodNotAllowed bool
    ForwardedByClientIP    bool
    AppEngine bool
    UseRawPath bool
    UnescapePathValues bool
    MaxMultipartMemory int64
    delims           render.Delims
    secureJsonPrefix string
    HTMLRender       render.HTMLRender
    FuncMap          template.FuncMap
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain
    // 对象池 用来创建上下文context
    pool             sync.Pool
    //记录路由方法的 比如GET POST 都会是数组中的一个 每个方法对应一个基数树的一个root的node
    trees            methodTrees
}

Default方法其实就是创建了该对象,并添加了一些默认中间件:

func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

注意,这里Default方法内部调用了New方法,该方法默认添加了路由组"/"

func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine,
        UseRawPath:             false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJsonPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine
    engine.pool.New = func() interface{} {
        return engine.allocateContext()
    }
    return engine
}

context对象存储了上下文信息,包括:engine指针、request对象,responsewriter对象等,context在请求一开始就被创建,且贯穿整个执行过程,包括中间件、路由等等:

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

    Params   Params
    handlers HandlersChain
    index    int8

    engine *Engine

    // Keys is a key/value pair exclusively for the context of each request.
    Keys map[string]interface{}

    // Errors is a list of errors attached to all the handlers/middlewares who used this context.
    Errors errorMsgs

    // Accepted defines a list of manually accepted formats for content negotiation.
    Accepted []string
}

接下来是Use方法:

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    //调用routegroup的use方法
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    //为group的handlers添加中间件 
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

最后到达最终路由,有GET,POST等多种方法,但是每个方法的处理方式都是相同的,即把group和传入的handler合并,计算出路径存入tree中等待客户端调用:

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    //调用get方法
    return group.handle("GET", relativePath, handlers)
}

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    //计算路径地址,比如group地址是 router.Group("/api")
    //结果为/api/test/ 就是最终计算出来的结果 使用path.join 方法拼接 其中加了一些判断
    absolutePath := group.calculateAbsolutePath(relativePath)
    //把group中的handler和传入的handler合并 
    handlers = group.combineHandlers(handlers)
    //把方法 路径 和处理方法作为node 加入到基数树种,基数树在下次单独学习分析
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    finalSize := len(group.Handlers) + len(handlers)
    if finalSize >= int(abortIndex) {
        panic("too many handlers")
    }
    mergedHandlers := make(HandlersChain, finalSize)
    copy(mergedHandlers, group.Handlers)
    copy(mergedHandlers[len(group.Handlers):], handlers)
    return mergedHandlers
}

run方法则是启动服务,在http包中会有一个for逻辑不停的监听端口:

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
}

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

二 书写类似源码

一 Gin对象的构建

Gin框架是基于golang官方的http包搭建起来的,http包最重要的实现是:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

利用该方法,以及参数中的Handler接口,实现Gin的Engine,Context:

package engine

import "net/http"

type Engine struct {

}

func (e *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {

}

context对象其实就是对ServeHTTP方法参数的封装,因为这2个参数在web开发中一个完整请求链中都会用到:

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

type responseWriter struct {
    http.ResponseWriter
    size        int
    status      int
}

这里多了一个属性 writermem,如果仅仅只从功能上考虑,这里有了Request、Writer已经足够使用了,但是框架需要应对多变的返回数据情况,必须对其进行封装,比如:

type ResponseWriter interface {

    http.ResponseWriter

    Pusher() http.Pusher

    Status() int
    Size() int
    WriteString(string) (int, error)
    Written() bool
    WriteHeaderNow()
}

这里对外暴露的是接口RespnserWriter,内部的http.ResponseWriter实现原生的ResponseWriter接口,在reset()的时候进行拷贝即可:

func (c *Context) reset() {
    c.Writer = &c.writermem
    c.Params = c.Params[0:0]
    c.handlers = nil
    c.index = -1
    c.Keys = nil
    c.Errors = c.Errors[0:0]
    c.Accepted = nil
}

这样做能够更好的符合面向接口编程的概念。

Context也可以通过对象池复用提升性能:

type Engine struct {
    pool             sync.Pool
}

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)
}

紧接着就可以在Context的基础上实现其大量的常用方法了:

func (c *Context) Param(key string) string{
    return ""
}

func (c *Context) Query(key string) string {
    return ""
}

func (c *Context) DefaultQuery(key, defaultValue string) string {
    return ""
}


func (c *Context) PostFormArray(key string) []string{
    return nil
}

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:voidFan

查看原文:gin框架总结

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2881 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传