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

参数验证

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

gin 目前是使用 go-playground/validator 这个框架,截止目前,默认是使用 v10 版本;具体用法可以看看 validator package · go.dev 文档说明哦

下面以一个单元测试,简单说明下如何在tag里验证前端传递过来的数据

简单的例子

  1. func TestValidation(t *testing.T) {
  2. ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
  3. testCase := []struct {
  4. msg string // 本测试用例的说明
  5. jsonStr string // 输入的参数
  6. haveErr bool // 是否有 error
  7. bindStruct interface{} // 被绑定的结构体
  8. errMsg string // 如果有错,错误信息
  9. }{
  10. {
  11. msg: "数据正确: ",
  12. jsonStr: `{"a":1}`,
  13. haveErr: false,
  14. bindStruct: &struct {
  15. A int `json:"a" binding:"required"`
  16. }{},
  17. },
  18. {
  19. msg: "数据错误: 缺少required的参数",
  20. jsonStr: `{"b":1}`,
  21. haveErr: true,
  22. bindStruct: &struct {
  23. A int `json:"a" binding:"required"`
  24. }{},
  25. errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'required' tag",
  26. },
  27. {
  28. msg: "数据正确: 参数是数字并且范围 1 <= a <= 10",
  29. jsonStr: `{"a":1}`,
  30. haveErr: false,
  31. bindStruct: &struct {
  32. A int `json:"a" binding:"required,max=10,min=1"`
  33. }{},
  34. },
  35. {
  36. msg: "数据错误: 参数数字不在范围之内",
  37. jsonStr: `{"a":1}`,
  38. haveErr: true,
  39. bindStruct: &struct {
  40. A int `json:"a" binding:"required,max=10,min=2"`
  41. }{},
  42. errMsg: "Key: 'A' Error:Field validation for ‘A’ failed on the ‘min’ tag",
  43. },
  44. {
  45. msg: "数据正确: 不等于列举的参数",
  46. jsonStr: `{"a":1}`,
  47. haveErr: false,
  48. bindStruct: &struct {
  49. A int `json:"a" binding:"required,ne=10"`
  50. }{},
  51. },
  52. {
  53. msg: "数据错误: 不能等于列举的参数",
  54. jsonStr: `{"a":1}`,
  55. haveErr: true,
  56. bindStruct: &struct {
  57. A int `json:"a" binding:"required,ne=1,ne=2"` // ne 表示不等于
  58. }{},
  59. errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'ne' tag",
  60. },
  61. {
  62. msg: "数据正确: 需要大于10",
  63. jsonStr: `{"a":11}`,
  64. haveErr: false,
  65. bindStruct: &struct {
  66. A int `json:"a" binding:"required,gt=10"`
  67. }{},
  68. },
  69. // 总结: eq 等于,ne 不等于,gt 大于,gte 大于等于,lt 小于,lte 小于等于
  70. {
  71. msg: "参数正确: 长度为5的字符串",
  72. jsonStr: `{"a":"hello"}`,
  73. haveErr: false,
  74. bindStruct: &struct {
  75. A string `json:"a" binding:"required,len=5"` // 需要参数的字符串长度为5
  76. }{},
  77. },
  78. {
  79. msg: "参数正确: 为列举的字符串之一",
  80. jsonStr: `{"a":"hello"}`,
  81. haveErr: false,
  82. bindStruct: &struct {
  83. A string `json:"a" binding:"required,oneof=hello world"` // 需要参数是列举的其中之一,oneof 也可用于数字
  84. }{},
  85. },
  86. {
  87. msg: "参数正确: 参数为email格式",
  88. jsonStr: `{"a":"hello@gmail.com"}`,
  89. haveErr: false,
  90. bindStruct: &struct {
  91. A string `json:"a" binding:"required,email"`
  92. }{},
  93. },
  94. {
  95. msg: "参数错误: 参数不能等于0",
  96. jsonStr: `{"a":0}`,
  97. haveErr: true,
  98. bindStruct: &struct {
  99. A int `json:"a" binding:"gt=0|lt=0"`
  100. }{},
  101. errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'gt=0|lt=0' tag",
  102. },
  103. // 详情参考: https://pkg.go.dev/github.com/go-playground/validator/v10?tab=doc
  104. }
  105. for _, c := range testCase {
  106. ctx.Request = httptest.NewRequest("POST", "/", strings.NewReader(c.jsonStr))
  107. if c.haveErr {
  108. err := ctx.ShouldBindJSON(c.bindStruct)
  109. assert.Error(t, err)
  110. assert.Equal(t, c.errMsg, err.Error())
  111. } else {
  112. assert.NoError(t, ctx.ShouldBindJSON(c.bindStruct))
  113. }
  114. }
  115. }
  116. // 测试 form 的情况
  117. // time_format 这个tag 只能在 form tag 下能用
  118. func TestValidationForm(t *testing.T) {
  119. ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
  120. testCase := []struct {
  121. msg string // 本测试用例的说明
  122. formStr string // 输入的参数
  123. haveErr bool // 是否有 error
  124. bindStruct interface{} // 被绑定的结构体
  125. errMsg string // 如果有错,错误信息
  126. }{
  127. {
  128. msg: "数据正确: 时间格式",
  129. formStr: `a=2010-01-01`,
  130. haveErr: false,
  131. bindStruct: &struct {
  132. A time.Time `form:"a" binding:"required" time_format:"2006-01-02"`
  133. }{},
  134. },
  135. }
  136. for _, c := range testCase {
  137. ctx.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(c.formStr))
  138. ctx.Request.Header.Add("Content-Type", binding.MIMEPOSTForm) // 这个很关键
  139. if c.haveErr {
  140. err := ctx.ShouldBind(c.bindStruct)
  141. assert.Error(t, err)
  142. assert.Equal(t, c.errMsg, err.Error())
  143. } else {
  144. assert.NoError(t, ctx.ShouldBind(c.bindStruct))
  145. }
  146. }
  147. }

简单解释一下,还记得上一篇文章讲的单元测试吗,这里只需要使用到 gin.Context 对象,所以忽略掉 gin.CreateTestContext() 返回的第二个参数,但是需要将输入参数放进 gin.Context,也就是把 Request 对象设置进去 ,接下来才能使用 Bind 相关的方法哦。

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

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

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

日志

使用gin默认的日志

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

这个函数是这样定义的

  1. func Logger() HandlerFunc {
  2. return LoggerWithConfig(LoggerConfig{})
  3. }

继续跟源码,看来真正处理的就是 LoggerWithConfig() 函数了,下面列出部分关键源码

  1. func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
  2. formatter := conf.Formatter
  3. if formatter == nil {
  4. formatter = defaultLogFormatter
  5. }
  6. out := conf.Output
  7. if out == nil {
  8. out = DefaultWriter
  9. }
  10. notlogged := conf.SkipPaths
  11. isTerm := true
  12. if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
  13. (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
  14. isTerm = false
  15. }
  16. var skip map[string]struct{}
  17. if length := len(notlogged); length > 0 {
  18. skip = make(map[string]struct{}, length)
  19. for _, path := range notlogged {
  20. skip[path] = struct{}{}
  21. }
  22. }
  23. return func(c *Context) {
  24. // Start timer
  25. start := time.Now()
  26. path := c.Request.URL.Path
  27. raw := c.Request.URL.RawQuery
  28. // Process request
  29. c.Next()
  30. // Log only when path is not being skipped
  31. if _, ok := skip[path]; !ok {
  32. // 中间省略这一大块是在处理打印的逻辑
  33. // ……
  34. fmt.Fprint(out, formatter(param)) // 最后是通过 重定向到 out 进行输出
  35. }
  36. }
  37. }

稍微解释下,函数入口传参是 LoggerConfig 这个定义如下:

  1. type LoggerConfig struct {
  2. Formatter LogFormatter
  3. Output io.Writer
  4. SkipPaths []string
  5. }

而调用 Default() 初始化gin时候,这个结构体是一个空结构体,在 LoggerWithConfig 函数中,如果这个结构体内容为空,会为它设置一些默认值

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

使用第三方的日志

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

logrus

GitHub主页

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

  1. func main() {
  2. g := gin.Default()
  3. gin.DisableConsoleColor()
  4. testLogrus(g)
  5. if err := g.Run(); err != nil {
  6. panic(err)
  7. }
  8. }
  9. func testLogrus(g *gin.Engine) {
  10. log := logrus.New()
  11. file, err := os.Create("mylog.txt")
  12. if err != nil {
  13. fmt.Println("err:", err.Error())
  14. os.Exit(0)
  15. }
  16. log.SetOutput(io.MultiWriter(os.Stdout, file))
  17. logMid := func() gin.HandlerFunc {
  18. return func(ctx *gin.Context) {
  19. var data string
  20. if ctx.Request.Method == http.MethodPost { // 如果是post请求,则读取body
  21. body, err := ctx.GetRawData() // body 只能读一次,读出来之后需要重置下 Body
  22. if err != nil {
  23. log.Fatal(err)
  24. }
  25. ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 重置body
  26. data = string(body)
  27. }
  28. start := time.Now()
  29. ctx.Next()
  30. cost := time.Since(start)
  31. log.Infof("方法: %s, URL: %s, CODE: %d, 用时: %dus, body数据: %s",
  32. ctx.Request.Method, ctx.Request.URL, ctx.Writer.Status(), cost.Microseconds(), data)
  33. }
  34. }
  35. g.Use(logMid())
  36. // curl 'localhost:8080/send'
  37. g.GET("/send", func(ctx *gin.Context) {
  38. ctx.JSON(200, gin.H{"msg": "ok"})
  39. })
  40. // curl -XPOST 'localhost:8080/send' -d 'a=1'
  41. g.POST("/send", func(ctx *gin.Context) {
  42. ctx.JSON(200, gin.H{"a": ctx.PostForm("a")})
  43. })
  44. }

zap

zap文档

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

GO语言web框架Gin之完全指南(二)的更多相关文章

  1. GO语言web框架Gin之完全指南

    GO语言web框架Gin之完全指南 作为一款企业级生产力的web框架,gin的优势是显而易见的,高性能,轻量级,易用的api,以及众多的使用者,都为这个框架注入了可靠的因素.截止目前为止,github ...

  2. GO语言web框架Gin之完全指南(一)

    作为一款企业级生产力的web框架,gin的优势是显而易见的,高性能,轻量级,易用的api,以及众多的使用者,都为这个框架注入了可靠的因素.截止目前为止,github上面已经有了 35,994 star ...

  3. Go语言web框架 gin

    Go语言web框架 GIN gin是go语言环境下的一个web框架, 它类似于Martini, 官方声称它比Martini有更好的性能, 比Martini快40倍, Ohhhh….看着不错的样子, 所 ...

  4. 最好的6个Go语言Web框架

    原文:Top 6 web frameworks for Go as of 2017 作者:Edward Marinescu 译者:roy 译者注:本文介绍截至目前(2017年)最好的6个Go语言Web ...

  5. Go语言Web框架gwk介绍4

    Go语言Web框架gwk介绍 (四)   事件 gwk支持事件系统,但并没有硬编码有哪些事件,而是采用了比较松散的定义方式. 订阅事件有两种方式: 调用On函数或者OnFunc函数 func On(m ...

  6. Go语言Web框架gwk介绍 3

    Go语言Web框架gwk介绍 (三)   上一篇忘了ChanResult ChanResult 可以用来模拟BigPipe,定义如下 type ChanResult struct { Wait syn ...

  7. Go语言Web框架gwk介绍2

    Go语言Web框架gwk介绍 (二) HttpResult 凡是实现了HttpResult接口的对象,都可以作为gwk返回Web客户端的内容.HttpResult接口定义非常简单,只有一个方法: ty ...

  8. Go语言Web框架gwk介绍 1

    Go语言Web框架gwk介绍 (一)   今天看到Golang排名到前30名了,看来关注的人越来越多了,接下来几天详细介绍Golang一个web开发框架GWK. 现在博客园支持markdown格式发布 ...

  9. Go组件学习——Web框架Gin

    以前学Java的时候,和Spring全家桶打好关系就行了,从Spring.Spring MVC到SpringBoot,一脉相承. 对于一个Web项目,使用Spring MVC,就可以基于MVC的思想开 ...

随机推荐

  1. ActiveMQ学习总结(一)

    自己写的网上商城项目中使用了ActiveMQ,虽然相比于RabbitMQ,kafka,RocketMQ等相比,ActiveMQ可能性能方面不是最好的选择,不过消息队列其实原理区别不大,这里对学过的关于 ...

  2. ES6中Map数据结构学习笔记

    很多东西就是要细细的品读然后做点读书笔记,心理才会踏实- Javascript对象本质上就是键值对的集合(Hash结构),但是键只能是字符串,这有一定的限制. 1234 var d = {}var e ...

  3. Opengl-法线贴图(用来细化表面的表现表现的凹凸)

    我们通过这张图可以看出来,使用了法线贴图的物体表面更有细节更逼真,其实这就是发现贴图的作用,没什么钻牛角尖的. 其实表面没有凹凸的情况是因为我们把表面一直按照平整来做的,要想突出这个表面的凹凸就要用到 ...

  4. JSON parse error: Cannot deserialize value of type `java.util.Date` from String

    DateTimePicker + @DateTimeFormat("yyyy-MM-dd HH:mm:ss")日期格式转换异常 最近在做的一个项目使用的日期格式是yyyy-MM-d ...

  5. C语言链表的基本操作

    */ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...

  6. marquee用到的属性

      一.marquee标签的几个重要属性: 1.direction:滚动方向(包括4个值:up.down.left.right) 说明:up:从下向上滚动:down:从上向下滚动:left:从右向左滚 ...

  7. 冒泡排序算法(C#、Java、Python、JavaScript、C、C++实现)

    一.介绍 它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小.首字母从Z到A)错误就把他们交换过来. 走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排 ...

  8. 使用 EOLINKER 进行接口测试的最佳路径 (上)

    本文内容: 测试脚本管理:讲述如何在 EOLINKER 上设计测试项目目录结构. 编写测试脚本:讲述如何在 EOLINKER 上编写接口测试脚本. 测试脚本执行及报告:讲述如何在 EOLINKER 上 ...

  9. C#版免费离线人脸识别——虹软ArcSoft V3.0

    [温馨提示] 本文共678字(不含代码),8张图.预计阅读时间需要6分钟. 1. 前言 人脸识别&比对发展到今天,已经是一个非常成熟的技术了,而且应用在生活的方方面面,比如手机.车站.天网等. ...

  10. Adobe Premiere Pro 2020破解教程

    首先官网下载Adobe Creative Cloud,安装完之后使用它继续安装Pr.注意在安装之前,点击文件→首选项,先设置一下你的安装路径,没有设置则默认安装在C盘. 接着下载网上良心博主推荐的破解 ...