内容简介:这篇主要讲解我们知道,一个请求完全依赖前端的参数验证是不够的,需要前后端一起配合,才能万无一失,下面介绍一下,在Gin框架里面,怎么做接口参数验证的呢gin 目前是使用
这篇主要讲解 自定义日志 与 数据验证
参数验证
我们知道,一个请求完全依赖前端的参数验证是不够的,需要前后端一起配合,才能万无一失,下面介绍一下,在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
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公司主导开发的,这里就不单独举例子了,可与参考下 zap中间件 的实现
以上所述就是小编给大家介绍的《GO语言web框架Gin之完全指南(二)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- go语言学习爬虫框架总结
- 使用 Go 语言实现一个异步任务框架
- Go语言开发(十九)、GoConvey测试框架
- Go语言开发(二十)、GoStub测试框架
- Go语言web框架学习—Gin
- Go语言开发(二十一)、GoMock测试框架
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
单元测试之道Java版
David Thomas、Andrew Hunt / 陈伟柱、陶文 / 电子工业 / 2005-1 / 25.00元
程序员修炼三部曲丛书包含了四本书,介绍了每个注重实效的程序员和成功团队所必备的一些工具。 注重实效的程序员都会利用反馈来指导开发,并驱动个人的开发流程。编码的时候,最有用的反馈来自于“单元测试”。 为了测试一座桥梁,不会只在晴朗的天气,开一辆汽车从桥中间穿过,就认为已经完成了对桥梁的测试。然而许多程序员却正在使用这种测试方法——把这种一次顺利通过称为“测试”。事实上,注重实效的程序员应......一起来看看 《单元测试之道Java版》 这本书的介绍吧!