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

参数验证

我们知道,一个请求完全依赖前端的参数验证是不够的,需要前后端一起配合,才能万无一失,下面介绍一下,在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中间件 的实现

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. mac命令日常总结

    查看某个端口被占用 lsof -i tcp:8080 kill进程: 找到进程的PID,使用kill命令:kill -9 716(PID) date 显示系统日期 mkdir xx 创建xx目录 rm ...

  2. MongoDB的初级安装和使用

    对于非关系型数据库,很多人不是很清楚,我也是作为新手慢慢摸索, 外网地址下载贼慢:我烦放在自己的百度网盘里 软件链接:https://pan.baidu.com/s/1A7h4VOfvm8N2gnlJ ...

  3. 吴裕雄--天生自然python编程:turtle模块绘图(2)

    #彩色螺旋线 import turtle import time turtle.pensize(2) turtle.bgcolor("black") colors = [" ...

  4. 由生到死10个月!做App中的“二”有多难

    十月,原本是怀胎过程的喜悦时光,但这段个时光,如今却是绝大多数App从生到死的所有时间.在App市场表面形式一片大好,彻底主宰我们生活.工作.娱乐的当下,绝大多数用户只是在App海洋中只取一瓢饮,其他 ...

  5. QA、EPG、PMO各自的职能划分及关系是什么?

    团队 职能 主要工作内容 EPG 负责过程持续改进工作 公司规范的建设和推广,并持续改进.收集过程改进需求,制定过程改进计划,获得高层的支持,并实施改进工作. PMO 负责公司内所有项目的审核.管理 ...

  6. 玩转iOS开发:iOS中的GCD开发(三)

    上一章, 我们了解到了GCD里的一些队列和任务的知识, 也实践了一下, 同时我们也对主队列的一些小情况了解了一下, 比如上一章讲到的卡线程的问题, 如果没有看的朋友可以去看看玩转iOS开发:iOS中的 ...

  7. 网络编程01 · 艺

    Web Socket和Socket 套接字,实际就是传输层的接口.用于抽象传输层,隐藏细节.一对套接字可以进行通信. Web Socket,是基于TCP协议的.类似于,http. 为什么需要Web S ...

  8. 一位资深程序员大牛推荐的Java技术学习路线图

    Web应用,最常见的研发语言是Java和PHP. 后端服务,最常见的研发语言是Java和C/C++. 大数据,最常见的研发语言是Java和Python. 可以说,Java是现阶段中国互联网公司中,覆盖 ...

  9. Python学习笔记--装饰器的实验

    装饰器既然可以增加原来函数的功能,那能不能改变传给原函数的参数呢? 我们实验一下,先上代码: #!/usr/bin/env python # -*- coding: utf-8 -*- # @Date ...

  10. Python-PhantomJS的安装和使用

    PhantomJS无需浏览器的Web测试: PhantomJS官网下载地址:https://phantomjs.org/download.html 下载PhantomJS zip文件,解压放置在D:\ ...