您的位置:首页 > 编程语言 > Go语言

GO语言web框架Gin之完全指南(二)

2020-04-24 08:15 791 查看

文章目录

  • 日志

  • 这篇主要讲解自定义日志数据验证

    参数验证

    我们知道,一个请求完全依赖前端的参数验证是不够的,需要前后端一起配合,才能万无一失,下面介绍一下,在Gin框架里面,怎么做接口参数验证的呢

    gin 目前是使用 go-playground/validator 这个框架,截止目前,默认是使用

    v10
    版本;具体用法可以看看 validator package · go.dev 文档说明哦

    下面以一个单元测试,简单说明下如何在

    tag
    里验证前端传递过来的数据

    简单的例子

    func TestValidation(t *testing.T) {
    ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
    
    testCase := []struct {
    msg        string      // 本测试用例的说明
    jsonStr    string      // 输入的参数
    haveErr    bool        // 是否有 error
    bindStruct interface{} // 被绑定的结构体
    errMsg     string      // 如果有错,错误信息
    }{
    {
    msg:     "数据正确: ",
    jsonStr: `{"a":1}`,
    haveErr: false,
    bindStruct: &struct {
    A int `json:"a" binding:"required"`
    }{},
    },
    {
    msg:     "数据错误: 缺少required的参数",
    jsonStr: `{"b":1}`,
    haveErr: true,
    bindStruct: &struct {
    A int `json:"a" binding:"required"`
    }{},
    errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'required' tag",
    },
    {
    msg:     "数据正确: 参数是数字并且范围 1 <= a <= 10",
    jsonStr: `{"a":1}`,
    haveErr: false,
    bindStruct: &struct {
    A int `json:"a" binding:"required,max=10,min=1"`
    }{},
    },
    {
    msg:     "数据错误: 参数数字不在范围之内",
    jsonStr: `{"a":1}`,
    haveErr: true,
    bindStruct: &struct {
    A int `json:"a" binding:"required,max=10,min=2"`
    }{},
    errMsg: "Key: 'A' Error:Field validation for ‘A’ failed on the ‘min’ tag",
    },
    {
    msg:     "数据正确: 不等于列举的参数",
    jsonStr: `{"a":1}`,
    haveErr: false,
    bindStruct: &struct {
    A int `json:"a" binding:"required,ne=10"`
    }{},
    },
    {
    msg:     "数据错误: 不能等于列举的参数",
    jsonStr: `{"a":1}`,
    haveErr: true,
    bindStruct: &struct {
    A int `json:"a" binding:"required,ne=1,ne=2"` // ne 表示不等于
    }{},
    errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'ne' tag",
    },
    {
    msg:     "数据正确: 需要大于10",
    jsonStr: `{"a":11}`,
    haveErr: false,
    bindStruct: &struct {
    A int `json:"a" binding:"required,gt=10"`
    }{},
    },
    // 总结: eq 等于,ne 不等于,gt 大于,gte 大于等于,lt 小于,lte 小于等于
    {
    msg:     "参数正确: 长度为5的字符串",
    jsonStr: `{"a":"hello"}`,
    haveErr: false,
    bindStruct: &struct {
    A string `json:"a" binding:"required,len=5"` // 需要参数的字符串长度为5
    }{},
    },
    {
    msg:     "参数正确: 为列举的字符串之一",
    jsonStr: `{"a":"hello"}`,
    haveErr: false,
    bindStruct: &struct {
    A string `json:"a" binding:"required,oneof=hello world"` // 需要参数是列举的其中之一,oneof 也可用于数字
    }{},
    },
    {
    msg:     "参数正确: 参数为email格式",
    jsonStr: `{"a":"hello@gmail.com"}`,
    haveErr: false,
    bindStruct: &struct {
    A string `json:"a" binding:"required,email"`
    }{},
    },
    {
    msg:     "参数错误: 参数不能等于0",
    jsonStr: `{"a":0}`,
    haveErr: true,
    bindStruct: &struct {
    A int `json:"a" binding:"gt=0|lt=0"`
    }{},
    errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'gt=0|lt=0' tag",
    },
    // 详情参考: https://pkg.go.dev/github.com/go-playground/validator/v10?tab=doc
    }
    
    for _, c := range testCase {
    ctx.Request = httptest.NewRequest("POST", "/", strings.NewReader(c.jsonStr))
    
    if c.haveErr {
    err := ctx.ShouldBindJSON(c.bindStruct)
    assert.Error(t, err)
    assert.Equal(t, c.errMsg, err.Error())
    } else {
    assert.NoError(t, ctx.ShouldBindJSON(c.bindStruct))
    }
    }
    }
    
    // 测试 form 的情况
    // time_format 这个tag 只能在 form tag 下能用
    func TestValidationForm(t *testing.T) {
    ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
    
    testCase := []struct {
    msg        string      // 本测试用例的说明
    formStr    string      // 输入的参数
    haveErr    bool        // 是否有 error
    bindStruct interface{} // 被绑定的结构体
    errMsg     string      // 如果有错,错误信息
    }{
    {
    msg:     "数据正确: 时间格式",
    formStr: `a=2010-01-01`,
    haveErr: false,
    bindStruct: &struct {
    A time.Time `form:"a" binding:"required" time_format:"2006-01-02"`
    }{},
    },
    }
    
    for _, c := range testCase {
    ctx.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(c.formStr))
    ctx.Request.Header.Add("Content-Type", binding.MIMEPOSTForm) // 这个很关键
    
    if c.haveErr {
    err := ctx.ShouldBind(c.bindStruct)
    assert.Error(t, err)
    assert.Equal(t, c.errMsg, err.Error())
    } else {
    assert.NoError(t, ctx.ShouldBind(c.bindStruct))
    }
    }
    }

    简单解释一下,还记得上一篇文章讲的单元测试吗,这里只需要使用到

    gin.Context
    对象,所以忽略掉
    gin.CreateTestContext()
    返回的第二个参数,但是需要将
    输入参数
    放进
    gin.Context
    ,也就是把
    Request
    对象设置进去 ,接下来才能使用
    Bind
    相关的方法哦。

    其中

    binding:
    代替框架文档中的
    validate
    ,因为gin单独给验证设置了tag名称,可以参考gin源码
    binding/default_validator.go

    func (v *defaultValidator) lazyinit() {
    v.once.Do(func() {
    v.validate = validator.New()
    v.validate.SetTagName("binding") // 这里改为了 binding
    })
    }

    上面的单元测试已经把基本的验证语法都列出来了,剩余的可以根据自身需求查询文档进行的配置

    日志

    使用gin默认的日志

    首先来看看,初始化gin的时候,使用了

    gin.Deatult()
    方法,上一篇文章讲过,此时默认使用了2个全局中间件,其中一个就是日志相关的
    Logger()
    函数,返回了日志处理的中间件

    这个函数是这样定义的

    func Logger() HandlerFunc {
    return LoggerWithConfig(LoggerConfig{})
    }

    继续跟源码,看来真正处理的就是

    LoggerWithConfig()
    函数了,下面列出部分关键源码

    func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
    formatter := conf.Formatter
    if formatter == nil {
    formatter = defaultLogFormatter
    }
    
    out := conf.Output
    if out == nil {
    out = DefaultWriter
    }
    
    notlogged := conf.SkipPaths
    
    isTerm := true
    
    if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
    (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
    isTerm = false
    }
    
    var skip map[string]struct{}
    
    if length := len(notlogged); length > 0 {
    skip = make(map[string]struct{}, length)
    
    for _, path := range notlogged {
    skip[path] = struct{}{}
    }
    }
    
    return func(c *Context) {
    // Start timer
    start := time.Now()
    path := c.Request.URL.Path
    raw := c.Request.URL.RawQuery
    
    // Process request
    c.Next()
    
    // Log only when path is not being skipped
    if _, ok := skip[path]; !ok {
    // 中间省略这一大块是在处理打印的逻辑
    // ……
    fmt.Fprint(out, formatter(param)) // 最后是通过 重定向到 out 进行输出
    }
    }
    }

    稍微解释下,函数入口传参是

    LoggerConfig
    这个定义如下:

    type LoggerConfig struct {
    Formatter LogFormatter
    Output io.Writer
    SkipPaths []string
    }

    而调用

    Default()
    初始化gin时候,这个结构体是一个空结构体,在
    LoggerWithConfig
    函数中,如果这个结构体内容为空,会为它设置一些默认值
    默认日志输出是到
    stdout
    的,默认打印格式是由
    defaultLogFormatter
    这个函数变量控制的,如果想要改变日志输出,比如同时输出到
    文件
    stdout
    ,可以在调用
    Default()
    之前,设置
    DefaultWriter
    这个变量;但是如果需要修改日志格式,则不能调用
    Default()
    了,可以调用
    New()
    初始化gin之后,使用
    LoggerWithConfig()
    函数,将自己定义的
    LoggerConfig
    传入。

    使用第三方的日志

    默认gin只会打印到

    stdout
    ,我们如果使用第三方的日志,则不需要管gin本身的输出,因为它不会输出到文件,正常使用第三方的日志工具即可。由于第三方的日志工具,我们需要实现一下 gin 本身打印接口(比如接口时间,接口名称,path等等信息)的功能,所以往往需要再定义一个中间件去打印。

    logrus

    GitHub主页

    logrus 是一个比较优秀的日志框架,下面这个例子简单的使用它来记录下日志

    func main() {
    g := gin.Default()
    gin.DisableConsoleColor()
    
    testLogrus(g)
    
    if err := g.Run(); err != nil {
    panic(err)
    }
    }
    
    func testLogrus(g *gin.Engine) {
    log := logrus.New()
    
    file, err := os.Create("mylog.txt")
    if err != nil {
    fmt.Println("err:", err.Error())
    os.Exit(0)
    }
    
    log.SetOutput(io.MultiWriter(os.Stdout, file))
    
    logMid := func() gin.HandlerFunc {
    return func(ctx *gin.Context) {
    var data string
    if ctx.Request.Method == http.MethodPost { // 如果是post请求,则读取body
    body, err := ctx.GetRawData() // body 只能读一次,读出来之后需要重置下 Body
    if err != nil {
    log.Fatal(err)
    }
    ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 重置body
    
    data = string(body)
    }
    
    start := time.Now()
    ctx.Next()
    cost := time.Since(start)
    
    log.Infof("方法: %s, URL: %s, CODE: %d, 用时: %dus, body数据: %s",
    ctx.Request.Method, ctx.Request.URL, ctx.Writer.Status(), cost.Microseconds(), data)
    }
    }
    
    g.Use(logMid())
    
    // curl 'localhost:8080/send'
    g.GET("/send", func(ctx *gin.Context) {
    ctx.JSON(200, gin.H{"msg": "ok"})
    })
    
    // curl -XPOST 'localhost:8080/send' -d 'a=1'
    g.POST("/send", func(ctx *gin.Context) {
    ctx.JSON(200, gin.H{"a": ctx.PostForm("a")})
    })
    }

    zap

    zap文档
    zap同样是比较优秀的日志框架,是由uber公司主导开发的,这里就不单独举例子了,可与参考下 zap中间件 的实现

    • 点赞 2
    • 收藏
    • 分享
    • 文章举报
    IM_SEAN_YANG 发布了2 篇原创文章 · 获赞 4 · 访问量 163 私信 关注
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: