Gin 默认用就是 go-playground/validator 这个库, 通过 tag 可以设置结构体字段的校验规则, go-playground/validator 自带了差不多一百种吧, 比如必须有值(required), 验证长度(len), 有效邮箱(email), 如果这些都还不能满足的话还能够根据需要自定义.

1
2
3
4
type Category struct {
    Name string `form:"name" json:"name" binding:"required"`
    Slug string `form:"slug" json:"slug" binding:"required"`
}

这里 binding 是 v8 版本的写法, 也就是 gin 当前(2018.9)引用的 validator 版本, 但是, go-playground/validator 早就更新到了 v9 了, 并且 binding 换成了 validate, 还有其他一些用法因为变动比较大, 虽然老早有人提了 PR 但是目前貌似还合不了. go-playground/validator 索性自己给了一个升级方案出来.

v8 的自定义规则类似长这样的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func bookableDate(
    v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
    field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
    if date, ok := field.Interface().(time.Time); ok {
        today := time.Now()
        if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
            return false
        }
    }
    return true
}

一堆反射的参数看着头都晕了, 换成了 v9 以后简洁多了, 反射什么的按需自取就是了, 结合 gorm 写的一个判断数据库字段唯一的自定义规则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func ValidateUniq(fl validator.FieldLevel) bool {
    var result struct{ Count int }
    currentField, _, _ := fl.GetStructFieldOK()
    table := modelTableNameMap[currentField.Type().Name()] // table name
    value := fl.Field().String()                           // value
    column := fl.FieldName()                               // column name
    sql := fmt.Sprintf("select count(*) from %s where %s='%s'", table, column, value)
    db.PG.Raw(sql).Scan(&result)
    dup := result.Count > 0
    return !dup
}

一个简单的示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package model

import (
    "coconut/db"
    "fmt"

    validator "gopkg.in/go-playground/validator.v9"

    "github.com/gin-gonic/gin/binding"

    "github.com/gin-gonic/gin"
    "github.com/jinzhu/gorm"
)

type Category struct {
    gorm.Model
    Name string `form:"name" json:"name" binding:"required,is-uniq"`
    Slug string `form:"slug" json:"slug" binding:"required"`
}

// CATEGORY VALIDATOR
type CategoryValidator struct {
    CategoryModel Category `json:"category"`
}

func (s *CategoryValidator) Bind(c *gin.Context) error {
    b := binding.Default(c.Request.Method, c.ContentType())
    err := c.ShouldBindWith(s, b)
    if err != nil {
        return err
    }
    return nil
}

func ValidateUniq(fl validator.FieldLevel) bool {
    var result struct{ Count int }
    currentField, _, _ := fl.GetStructFieldOK()
    table := modelTableNameMap[currentField.Type().Name()] // table name
    value := fl.Field().String()                           // value
    column := fl.FieldName()                               // column name
    sql := fmt.Sprintf("select count(*) from %s where %s='%s'", table, column, value)
    db.PG.Raw(sql).Scan(&result)
    dup := result.Count > 0
    return !dup
}

方法 Bind 返回的 error 是结构体下所有违规的字段错误, 所以可以这么处理(v9)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type CommonError struct {
    Errors map[string]interface{} `json:"errors"`
}

func NewValidatorError(err error) CommonError {
    res := CommonError{}
    res.Errors = make(map[string]interface{})
    errs := err.(validator.ValidationErrors)

    for _, e := range errs {
        res.Errors[e.Field()] = e.ActualTag()
    }
    return res
}