欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-07-06 12:39:29
这篇主要讲解 自定义日志 与 数据验证 参数验证 我们知道,一个请求完全依赖前端的参数验证是不够的,需要前后端一起配合,才能万无一失,下面介绍一下,在Gin框架里面,怎么做接口参数验证的呢 gin 目前是使用 "go playground/validator" 这个框架,截止目前,默认是使用 版本; ......

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

参数验证

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

gin 目前是使用 这个框架,截止目前,默认是使用 v10 版本;具体用法可以看看 文档说明哦

下面以一个单元测试,简单说明下如何在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同样是比较优秀的日志框架,是由uber公司主导开发的,这里就不单独举例子了,可与参考下 的实现