2021SC@SDUSC
引言
在 sduoj 项目中,服务端需要接收来自前端的请求,根据不同的请求来进行不同的业务处理。如果没有一个规范化的约束的话,服务端就有可能接收到不合理的请求。为了避免参数的错误导致业务逻辑的混乱,我们需要一种接口校验机制,来退回这些不合理的请求。
接口入参校验规则写在对应的校验结构体的字段标签上,在CreateTagRequest
中,我们看到这个它需要三个请求体参数,分别是Name
、CreatedBy
和State
。在字段标签中,form
中存放参数名,binding
存放规则约束。
type CreateTagRequest struct {
Name string `form:"name" binding:"required,min=3,max=100"`
CreatedBy string `form:"created_by" binding:"required,min=3,max=100"`
State uint8 `form:"state,default=1" binding:"oneof=0 1"`
}
以下是常见的标签含义。
标签 | 含义 |
---|---|
require | 必填 |
gt | 大于 |
gte | 大于等于 |
lt | 小于 |
lte | 小于等于 |
min | 最小值 |
max | 最大值 |
oneof | 参数集内的其中之一 |
len | 长度要求与 len 给定的一致 |
源码分析
BindAndValid
方法主要负责错误的处理,其参数绑定和校验工作放在c.ShouldBind
中。在错误处理中,该方法从上下文中获取trans
所对应的值,将错误信息进行一定的语言转化工作,并将这些错误信息存入ValidErrors
中。
func BindAndValid(c *gin.Context, v interface{
}) (bool, ValidErrors) {
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
}
for key, value := range verrs.Translate(trans) {
errs = append(errs, &ValidError{
Key: key,
Message: value,
})
}
return false, errs
}
return true, nil
}
validErrors
是[]*validError
的别名,在这里我们实现了*ValidError
的Error
方法,而error
接口只有Error
方法,也就是说,*ValidError
实现了error
接口。
type ValidError struct {
Key string
Message string
}
type ValidErrors []*ValidError
func (v *ValidError) Error() string {
return v.Message
}
ShouldBind
会根据Context
的类型自动选择参数绑定引擎,比如我们的Context
类型为 application/json 的话,我们就会选择 JSON 绑定引擎。
func (c *Context) ShouldBind(obj interface{
}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.ShouldBindWith(obj, b)
}
ShouldBindWith
会使用具体的绑定引擎来绑定通过的结构体指针。
func (c *Context) ShouldBindWith(obj interface{
}, b binding.Binding) error {
return b.Bind(c.Request, obj)
}
Value
方法返回了与key
的上下文有关的值。它先检测key
的值是否为 0,如果是的话,就把c.Request
返回出去;如果不是的话,就断言key
是字符串类型,并将相关的值返回,如果key
不是字符串类型,就返回nil
。
func (c *Context) Value(key interface{
}) interface{
} {
if key == 0 {
return c.Request
}
if keyAsString, ok := key.(string); ok {
val, _ := c.Get(keyAsString)
return val
}
return nil
}
Get
方法返回了给定key
所映射的值,期间涉及到一次加锁和解锁操作,这样可以保证获取这个值的操作是原子的。
func (c *Context) Get(key string) (value interface{
}, exists bool) {
c.mu.RLock()
value, exists = c.Keys[key]
c.mu.RUnlock()
return
}
在validator
中默认的错误信息是英文,但我们的错误信息不一定要用英文,这里可以换成简体中文。这里是自定义了一个中间件,根据locale
选择语言,然后将trans
存放在上下文中。
func Translations() gin.HandlerFunc {
return func(c *gin.Context) {
uni := ut.New(en.New(), zh.New(), zh_Hant_TW.New())
locale := c.GetHeader("locale")
trans, _ := uni.GetTranslator(locale)
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
switch locale {
case "zh":
_ = zh_translations.RegisterDefaultTranslations(v, trans)
case "en":
_ = en_translations.RegisterDefaultTranslations(v, trans)
default:
_ = zh_translations.RegisterDefaultTranslations(v, trans)
}
c.Set("trans", trans)
}
c.Next()
}
}