当我们去调用函数,或者发起网络调用时,被调用方一般都会进行防御式编程,对请求的参数字段进行校验,如果请求的字段比较多或复杂,那么请求校验的函数就会写得很长且复杂,代码间充斥着大量if-else语句,这样既不优雅也不利于维护。比如下面的这段请求检验函数:
package main
// main.go
import (
"fmt"
"errors"
//"github.com/go-playground/validator/v10"
)
type Address struct {
Street string
City string
Planet string
Phone string
}
type User struct {
FirstName string
LastName string
Age uint8
Email string
FavouriteColor string
Addresses []*Address
}
func requestCheck(req *User) error {
if req.FirstName == "" || req.LastName == "" {
return errors.New("name error")
}
if req.Email == "" {
return errors.New("email error")
}
if req.Age < 18 || req.Age > 60 {
return errors.New("age error")
}
if req.FavouriteColor != "green" && req.FavouriteColor != "blue" || req.FavouriteColor != "red" {
return errors.New("color error")
}
return nil
}
func main() {
address := &Address{
Street: "Eavesdown Docks",
Planet: "Persphone",
Phone: "none",
City: "Unknown",
}
user := &User{
FirstName: "James",
LastName: "Carter",
Age: 16,
Email: "Carter.James@gmail",
FavouriteColor: "yellow",
Addresses: []*Address{address},
}
err := requestCheck(user)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(user)
}
上面这个例子中,因为传入的参数的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其中之一。下面这个例子构造了三个请求结构体,分别对应三个数据检验错误:
- user1:FavouriteColor字段的值不属于指定的定义列表;
- user2:Age字段的值不再定义的区间内;
- 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