内容简介:这篇主要讲解我们知道,一个请求完全依赖前端的参数验证是不够的,需要前后端一起配合,才能万无一失,下面介绍一下,在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测试框架
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Machine Learning in Action
Peter Harrington / Manning Publications / 2012-4-19 / GBP 29.99
It's been said that data is the new "dirt"—the raw material from which and on which you build the structures of the modern world. And like dirt, data can seem like a limitless, undifferentiated mass. ......一起来看看 《Machine Learning in Action》 这本书的介绍吧!