您的位置:首页 > 编程语言 > Go语言

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

2020-03-23 16:40 369 查看

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

参数验证

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

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐