eddycjy/go-programming-tour-book-comments

为接口做参数校验 | Go 语言编程之旅

Opened this issue · 26 comments

为接口做参数校验 | Go 语言编程之旅

2.5 为接口做参数校验 接下来我们将正式进行编码,在进行对应的业务模块开发时,第一步要考虑到的问题的就是如何进行入参校验,我们需要将整个项目,甚至整个团队的组件给定下来,形成一个通用规范,在今天本章节将核心介绍这一块,并完成标签模块的接口的入参校验。
2.5.1 validator 介绍 在本项目中我们将使用开源项目

https://golang2.eddycjy.com/posts/ch2/05-validator/

是我看错了吗 errorf方法还没有再logger文件中定义

您好,文章写的非常棒!有个小问题,CountTagRequest 的tag为什么是binding:"max=100"而不是validate:"max=100"? 我看validator package里面的example都是用的validate作为tag的key?

一楼的同学 补充一下代码即可

func (l *Logger) Error(v ...interface{}) {
	l.Output(LevelError, fmt.Sprint(v...))
}

func (l *Logger) Errorf(format string, v ...interface{}) {
	l.Output(LevelError, fmt.Sprintf(format, v...))
}

买了实体书,也在线看,里面用到了很多第三方包,不先看一下第三方包的手册就一直看下去会很懵逼

买了实体书,也在线看,里面用到了很多第三方包,不先看一下第三方包的手册就一直看下去会很懵逼

@willnotlazy 确实,可以边看边查了解一下。因为这本书的定位是进阶,因此不会一个个的介绍第三方包。

是我看错了吗 errorf方法还没有再logger文件中定义

@yzawudi 文章里面提到了,因为受限于篇幅所以没有一个个代码展现出来。你只需要自己补充完善就好,因为代码都是雷同的。

一楼的同学 补充一下代码即可

func (l *Logger) Error(v ...interface{}) {
	l.Output(LevelError, fmt.Sprint(v...))
}

func (l *Logger) Errorf(format string, v ...interface{}) {
	l.Output(LevelError, fmt.Sprintf(format, v...))
}

@Ca2OH4 感谢答疑!

您好,文章写的非常棒!有个小问题,CountTagRequest 的tag为什么是binding:"max=100"而不是validate:"max=100"? 我看validator package里面的example都是用的validate作为tag的key?

@jiapengliu613 因为 binding 是 gin validator 的属性,validate 的 validator 的缺省。

translations.go break?

大佬 这个是啥情况了go16版本保证换成go14就没事
imported from implicitly required module

大佬 这个是啥情况了
go16版本报错
换成go14就没事
imported from implicitly required module

你好请问一个问题,就是我是初学者,没有什么项目经验,我的疑问就是,这么大的项目主要的掌握的是 对于路由分发(router.go) 路由方法(article.go) 全局变量(global/setting.go)中的内容编写么? 然后才是根据具体的需求去编写具体的模块么? 这段话我可能描述不清楚,更广泛的问题就是从这个项目中应该掌握什么? 感谢回答, 一路跌跌撞撞 代码慢慢跑通到这一节,但是对于内容的庞大感到吃力了 不知道如何去掌握,已经支持了实体书 感谢

response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
	global.Logger.Errorf("app.BindAndValid errs: %v", errs)
	response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
	return
}

response.ToResponse(gin.H{})

response 相关的代码在哪?

建议命名上做好区分度,太相近了..
func (v ValidErrors) Error() string {
return strings.Join(v.Errors(), ",")
}

func (v ValidErrors) Errors() []string {
var errs []string
for _, err := range v {
errs = append(errs, err.Error())
}

return errs

}

2.5.5中的 global.Logger.Errorf("app.BindAndValid errs: %v", errs) 语句
缺少了一个输入,应该修改为 global.Logger.Errorf(c, "app.BindAndValid errs: %v", errs)

很早之前在 validator v10.4.1 的时候发现了一个问题,如果 RegisterDefaultTranslations 放在中间件里调用,如果多个用户同时访问的时候,后端很容易就挂掉,出现 panic:concurrent map read and map write 并且无法 recovery,使用 ab 做压力测试可复现这个问题。

后来我改在 BindAndValid 函数里初始化 RegisterDefaultTranslations ,之后半年多都没有问题。今天突然又出现了 panic:concurrent map read and map write,更新到 v10.10.1 仍然无法通过测试,在错误日志中找到问题出现在 https://github.com/go-playground/validator/blob/master/validator_instance.go#L301 这个位置,并发的时候可能会同时访问到这个 map,进而导致 panic。

通过查阅 gin/binding 的代码:https://github.com/gin-gonic/gin/blob/2bde107686759098e2d64273bc79d1a0216a4500/binding/binding.go#L70,gin 在 package 内实例化了 validator。所以,如果在每个请求内都调用如下代码:

v, ok := binding.Validator.Engine().(*val.Validate)

v 的地址始终都是相同的。

再看 validator 的代码:https://github.com/go-playground/validator/blob/42525d89abaf198b1e377addede1aef4b6183fc1/validator_instance.go#L301 如果两次传入的 tag 是不一样的,就会造成在 panic:concurrent map read and map write

因此在并发的时候执行 _ = zhTranslations.RegisterDefaultTranslations(v, trans) 就会出现 panic:concurrent map read and map write

查阅 validator 的 issue 发现在之前 v9 版本的时候就有类似的问题 go-playground/validator#286 (comment)
建议在 package 的 init 函数内做初始化,然后我现在是这样初始化翻译器的:

var trans ut.Translator

func init() {
	uni := ut.New(zh.New())
	trans, _ = uni.GetTranslator("zh")
	v, ok := binding.Validator.Engine().(*val.Validate)
	if ok {
		_ = zhTranslations.RegisterDefaultTranslations(v, trans)
	}
}

func bindAndValid(c *gin.Context, target interface{}) bool {
    // ...
}

这样改了之后,再通过 ab 或 python 做并发测试就不会爆 panic 了

最后示例接口TagList绑定的时候为啥重复定义,不是已经定义好了TagListRequest嘛
直接var param TagListRequest

非常同意 @0xJacky 的意见,validator 不需要在中间件中重复注册翻译配置,只需要在项目启动的时候注册一次就行了,另BindAndValid方法中

var errs ValidErrors
err := c.ShouldBind(v)
if err != nil {
	v := c.Value("trans")
	trans, _ := v.(ut.Translator)
	verrs, ok := err.(val.ValidationErrors)
	if !ok {
		return false, errs
	}

verrs, ok := err.(val.ValidationErrors)ok为false时,返回的errs还是个空的ValidErrors,这时外部调用并不知道实际的err到底是啥。

上面大佬说的panic:concurrent map read and map write的问题,实际项目中踩坑了

global.Logger.Errorf("app.BindAndValid errs: %v", errs) 得改成global.Logger.Errorf(c, "app.BindAndValid errs: %v", errs),新版本好想要加入ctx了都

@0xJacky 踩坑了。。。。ab压测 一下就挂了

这里是internal/service吗还是internal/model呢

curl " http://127.0.0.1:8000/api/v1/tags\?state=6" -Method 'GET'
这样写才会报出示例的错误消息