当我们去调用函数,或者发起网络调用时,被调用方一般都会进行防御式编程,对请求的参数字段进行校验,如果请求的字段比较多或复杂,那么请求校验的函数就会写得很长且复杂,代码间充斥着大量if-else语句,这样既不优雅也不利于维护。比如下面的这段请求检验函数:

  1. package main
  2. // main.go
  3. import (
  4. "fmt"
  5. "errors"
  6. //"github.com/go-playground/validator/v10"
  7. )
  8. type Address struct {
  9. Street string
  10. City string
  11. Planet string
  12. Phone string
  13. }
  14. type User struct {
  15. FirstName string
  16. LastName string
  17. Age uint8
  18. Email string
  19. FavouriteColor string
  20. Addresses []*Address
  21. }
  22. func requestCheck(req *User) error {
  23. if req.FirstName == "" || req.LastName == "" {
  24. return errors.New("name error")
  25. }
  26. if req.Email == "" {
  27. return errors.New("email error")
  28. }
  29. if req.Age < 18 || req.Age > 60 {
  30. return errors.New("age error")
  31. }
  32. if req.FavouriteColor != "green" && req.FavouriteColor != "blue" || req.FavouriteColor != "red" {
  33. return errors.New("color error")
  34. }
  35. return nil
  36. }
  37. func main() {
  38. address := &Address{
  39. Street: "Eavesdown Docks",
  40. Planet: "Persphone",
  41. Phone: "none",
  42. City: "Unknown",
  43. }
  44. user := &User{
  45. FirstName: "James",
  46. LastName: "Carter",
  47. Age: 16,
  48. Email: "Carter.James@gmail",
  49. FavouriteColor: "yellow",
  50. Addresses: []*Address{address},
  51. }
  52. err := requestCheck(user)
  53. if err != nil {
  54. fmt.Println(err)
  55. return
  56. }
  57. fmt.Println(user)
  58. }

上面这个例子中,因为传入的参数的Age字段没有通过requestCheck的检验,因此输出为

age error

上面的请求检测函数requestCheck只对FirstName,LastName,Age,Email和FavouriteColor字段进行了逻辑检查,我们一般都是使用if-else结构进行判断,当请求结构体足够复杂或者判断逻辑异常复杂时,这个请求检验函数的代码量就会急速膨胀,变得不再优雅了。更重要的是,我们需要在我们的每个请求函数中都要加入这些请求检验函数,因为请求的数据结构不一样,因此需要为不同请求的字段进行各自的逻辑判断。这些都是重复性劳动,因此急需一种方式可以支持这类字段类型检查的工作。而Go的validate库可以很好地满足我们这类需求。

1. 使用validator库做请求检验

无论是网络调用还是本地的函数调用,我们面临的复杂请求都是基于结构体来承载的,因此这里首先以构体的验证为例,介绍validator库的基本用法。

validator库的核心用法如下,比如我们希望验证结构体的某些字段,那么我们可以在定义结构体时在字段类型后面加上一些描述tag,比如”validate:”required””,表明这个字段是不能为空。又比如”validate:”gte=0,lte=130””,表明这个字段必须处于区间0~130。如果不满足这些定义的条件,那么在调用validate.Struct时就会返回错误,而且这些错误是所有字段经过校验后得到的错误切片,因此我们可以通过这个错误切片知道具体是哪些字段校验失败了。我们可以利用这个特性对我们的请求结构体进行定制,哪些字段必须带值,哪些字段必须在某些范围内,都可以直接在定义结构体时完成,而无需额外写函数做请求检验,十分方便开发。

type Address struct {
    Street string `validate:"required"`
    City   string `validate:"required"`
    Planet string `validate:"required"`
    Phone  string `validate:"required"`
}

type User struct {
    FirstName      string     `validate:"required"`
    LastName       string     `validate:"required"`
    Age            uint8      `validate:"gte=0,lte=130"`
    Email          string     `validate:"required"`
    FavouriteColor string     `validate:"oneof=hexcolor rgb rgba"`
    Addresses      []*Address 
}

var validate *validator.Validate

err := validate.Struct(user)
if err != nil {
    fmt.Println(err)
    return
}

一个完整的例子如下,我们定义了一个略微复杂的结构体User,幷针对一些字段要求开启检验,比如FirstName我们要求是required,即必须带值;比如Age要求数值必须落在0~130区间;比如FavouriteColor只能是hexcolor,rgb,rgba其中之一。下面这个例子构造了三个请求结构体,分别对应三个数据检验错误:

  1. user1:FavouriteColor字段的值不属于指定的定义列表;
  2. user2:Age字段的值不再定义的区间内;
  3. user3:FirstName不能为空,而且FavouriteColor字段的值不属于指定的定义列表
package main

// main.go

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

type Address struct {
    Street string `validate:"required"`
    City   string `validate:"required"`
    Planet string `validate:"required"`
    Phone  string `validate:"required"`
}

type User struct {
    FirstName      string     `validate:"required"`
    LastName       string     `validate:"required"`
    Age            uint8      `validate:"gte=0,lte=130"`
    Email          string     `validate:"required"`
    FavouriteColor string     `validate:"oneof=hexcolor rgb rgba"`
    Addresses      []*Address 
}

var validate *validator.Validate

func main() {
    validate = validator.New()
    user1 := &User{
        FirstName:      "James",
        LastName:       "Carter",
        Age:            30,
        Email:          "Carter.James@gmail",
        FavouriteColor: "blue",
    }
    user2 := &User{
        FirstName:      "James",
        LastName:       "Carter",
        Age:            200,
        Email:          "Carter.James@gmail",
        FavouriteColor: "hexcolor",
    }
    user3 := &User{
        LastName:       "Carter",
        Age:            20,
        Email:          "Carter.James@gmail",
        FavouriteColor: "hexcolor",
    }

    err := validate.Struct(user1)
    if err != nil {
        fmt.Println("user1 check ", err)
    }

    err = validate.Struct(user2)
    if err != nil {
        fmt.Println("user2 check ", err)
    }

    err = validate.Struct(user3)
    if err != nil {
        fmt.Println("user3 check ", err)
    }

}

输出如下:

user1 check  Key: 'User.FavouriteColor' Error:Field validation for 'FavouriteColor' failed on the 'oneof' tag
user2 check  Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
user3 check  Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'required' tag

这里再介绍一些常用的validator tag语法:

type Address struct {
    Street string `validate:"required"`
    City   string `validate:"required"`
    Planet string `validate:"required"`
    Phone  string `validate:"required"`
}

type User struct {
    FirstName      string     `validate:"required"`
    LastName       string     `validate:"required"`
    Age            uint8      `validate:"gte=0,lte=130"`
    Email          string     `validate:"required"`
    FavouriteColor string     `validate:"oneof=hexcolor rgb rgba"`
    Addresses      []*Address `validate:"-"`  //对于内嵌的结构体,可以直接跳过检验
    Money           int            `validate:"omitempty,min=0,max=1000"`   //omitempt表示此字段可有可无,有的话才继续走后续的min,max的检测
    Members           []string        `validate:"gt=0,dive,len=1,dive,required"` //gt=0作用于[],len=1作用于[]string,required作用于string
    Scores         *map[string]int    `validate:"gt=0,dive,keys,eq=math|eq=science,endkeys,required"` //gt=0作用于map,eq=1|eq=2作用于map key,required作用于map value
}

我们根据User的结构体定义组装一些数据,传入validate.Struct进行验证测试:

user1 := &User{
    FirstName:      "James",
    LastName:       "Carter",
    Age:            30,
    Email:          "Carter.James@gmail",
    FavouriteColor: "blue",
    Money:            102000,
    Scores:            &map[string]int{"history":20},
    Members:        []string{},
}

输出如下,一共报了4个错误:

user1 check  Key: 'User.FavouriteColor' Error:Field validation for 'FavouriteColor' failed on the 'oneof' tag
Key: 'User.Money' Error:Field validation for 'Money' failed on the 'max' tag
Key: 'User.Members' Error:Field validation for 'Members' failed on the 'gt' tag
Key: 'User.Scores[history]' Error:Field validation for 'Scores[history]' failed on the 'eq=math|eq=science' tag

2. 利用validator做Json请求检验

validator不仅可以对结构体做检验,还可以结合Go的json包对Json字符串进行解析和后续检验,即先利用json包Unmarshal解析json转为结构体,再传给validator做请求检验。因为Json序列化协议在日常工作中广泛使用,因此validator做Json请求检验在Go的开源组件间中大量使用,比如Gin作为HTTP服务器在对HTTP请求的body 的检验就用了validator,只有通过validator检验的请求才会被binding到结构体上,传递给后面的逻辑处理。

下面这个就是json解析+validator检验的案例,首先我们封装了decodeJSON函数,里面包括两个操作:json反序列化为对象和validator结构体检验。

package main

// main.go

import (
    "fmt"
    "github.com/go-playground/validator/v10"
    "encoding/json"
)

type User struct {
    FirstName      string     `json:"fname" validate:"required"`
    LastName       string     `json:"lname" validate:"required"`
    Age            uint8      `json:"age" validate:"gte=0,lte=130"`
    Email          string     `json:"email" validate:"required"`
    FavouriteColor string     `json:"fcolor" validate:"oneof=hexcolor rgb rgba"`
}

func decodeJSON(data string, obj interface{}) error {
    err := json.Unmarshal([]byte(data), obj)
    if err != nil {
        return err
    }
    return validate.Struct(obj)
}

var validate *validator.Validate

func main() {
    validate = validator.New()

    var data = `{"fname":"XiLiaoXiao","lname":"Li","age":200,"email":"","fcolor":"rgb"}`
    user := User{}
    err := decodeJSON(data, &user)
    if err != nil {
        fmt.Println("decodeJSON err ", err)
        return
    }
    fmt.Println("decodeJSON ok ", user)
}

输出如下,因为json串的age数值不符合validator定义的数值区间,因此返回error,同样email字段为空,也是不符合我们定义的请求检验规则的,因此本次请求不能通过。

decodeJSON err  Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required' tag

我们修改下我们的json串数据后,就可以顺利通过请求检验了。

func main() {
    validate = validator.New()

    var data = `{"fname":"XiLiaoXiao","lname":"Li","age":100,"email":"lijunshi2015@163.com","fcolor":"rgb"}`
    user := User{}
    err := decodeJSON(data, &user)
    if err != nil {
        fmt.Println("decodeJSON err ", err)
        return
    }
    fmt.Println("decodeJSON ok ", user)
}

输出

decodeJSON ok  {XiLiaoXiao Li 100 lijunshi2015@163.com rgb}

3. 更多资料

这里的案例只是validator使用案例的冰山一角,这里只做抛砖引玉,更多的操作指南可以查阅官方文档:

https://pkg.go.dev/github.com/go-playground/validator/v10#readme-usage-and-documentation

同时推荐学习下Gin的请求检验和绑定的代码,Gin实现了多个场景的数据检验,其中包括:JSON,MsgPack,Protubuf,Xml等,很有学习价值:

https://github.com/gin-gonic/gin/tree/master/binding

官方项目example:

https://github.com/go-playground/validator/tree/master/_examples