在实际项目中,我们需要用到表单验证模块来验证前端传过来的数据是否合法,我这里用的是 go-playground/validator。这个库具有很多优点例如丰富的验证类型,错误信息多语言,并且他是 Gin 框架内置的验证器。因此我们可以在 Gin 中直接使用 validator 进行验证。
validate.go 的代码实现
package api
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
val "github.com/go-playground/validator/v10"
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
"log"
"net/http"
"reflect"
)
type ValidError struct {
Key string
Message string
}
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 {
errs := make(map[string]string)
err := c.ShouldBindJSON(target)
if err != nil {
log.Println("raw err", err)
verrs, ok := err.(val.ValidationErrors)
if !ok {
log.Println("verrs", verrs)
c.JSON(http.StatusNotAcceptable, gin.H{
"message": "请求参数错误",
"code": http.StatusNotAcceptable,
})
return false
}
for _, value := range verrs {
t := reflect.ValueOf(target)
realType := t.Type().Elem()
field, _ := realType.FieldByName(value.StructField())
errs[field.Tag.Get("json")] = value.Translate(trans)
}
c.JSON(http.StatusNotAcceptable, gin.H{
"errors": errs,
"message": "请求参数错误",
"code": http.StatusNotAcceptable,
})
return false
}
return true
}
之前看过许多博文,有的是在 Gin 路由中间件里做 zhTranslations.RegisterDefaultTranslations(v, trans)
的翻译器初始化,有的是在每个请求内的错误处理函数做初始化,但我是在 package init 函数内做翻译器的初始化。
前面两种方案都是错误的,第一种方案在并发请求时会出现 panic:concurrent map read and map write
,第二种方案在使用错误的参数做并发请求时会出现 panic:concurrent map read and map write
。这个问题可把我坑惨了,去年首次改用 validator 的时候就被第一个方案坑了,当时项目还在测试阶段,我们做内部访问的时候都会莫名其妙的挂掉,后来我就改在了错误处理函数内做翻译器的初始化,前几天线上环境后端突然挂了,最后保留的错误信息就是 validator 出现的 panic:concurrent map read and map write
,我在错误日志中找到问题出现在 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
得到的地址始终都是相同的,进一步的,后续调用的 v.transTagFunc
其实也都是同一个 map。
再看 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 了。
后记
对于这类问题,如果不好排查,可以使用 Apache Benchmark 或 Python 做模拟并发测试,可以很快的复现问题。
评论 (0)