Jacky's Blog Jacky's Blog
  • 首页
  • 关于
  • 项目
  • 大事记
  • 留言板
  • 友情链接
  • 分类
    • 干货
    • 随笔
    • 项目
    • 公告
    • 纪念
    • 尝鲜
    • 算法
    • 深度学习
首页 › 干货 › Gin validator 翻译器的初始化

Gin validator 翻译器的初始化

Jacky
4月 24, 2022干货阅读 666

在实际项目中,我们需要用到表单验证模块来验证前端传过来的数据是否合法,我这里用的是 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 做模拟并发测试,可以很快的复现问题。

赞(2)
本文系作者 @Jacky 原创发布在 Jacky's Blog。未经许可,禁止转载。
Go embed 静态文件
上一篇
容器监控方案 cAdvisor + Prometheus + Grafana
下一篇
再想想
暂无评论
近期评论
  • Jacky发表在《Nginx UI》
  • daiwenzh5发表在《Nginx UI》
  • Jacky发表在《Nginx UI》
  • daiwenzh5发表在《Nginx UI》
  • Jacky发表在《Nginx UI》
2
  • 2
  • 0
Copyright © 2016-2023 Jacky's Blog. Designed by nicetheme.
粤ICP备16016168号-1
  • 首页
  • 关于
  • 项目
  • 大事记
  • 留言板
  • 友情链接
  • 分类
    • 干货
    • 随笔
    • 项目
    • 公告
    • 纪念
    • 尝鲜
    • 算法
    • 深度学习
# Mac # # Apple # # OS X # # iOS # # macOS #
Jacky
PHP C C++ Python | 舞象之年 | 物联网工程
174
文章
169
评论
267
喜欢