golang 如何验证struct字段的数据格式

本文同时发表在github.com/zhangyachen…

假设我们有如下结构体:

1
2
3
4
5
6
复制代码type User struct {
Id int
Name string
Bio string
Email string
}

我们需要对结构体内的字段进行验证合法性:

  • Id的值在某一个范围内。
  • Name的长度在某一个范围内。
  • Email格式正确。

我们可能会这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码user := User{
Id: 0,
Name: "superlongstring",
Bio: "",
Email: "foobar",
}

if user.Id < 1 && user.Id > 1000 {
return false
}
if len(user.Name) < 2 && len(user.Name) > 10 {
return false
}
if !validateEmail(user.Email) {
return false
}

这样的话代码比较冗余,而且如果结构体新加字段,还需要再修改验证函数再加一段if判断。这样代码比较冗余。我们可以借助golang的structTag来解决上述的问题:

1
2
3
4
5
6
复制代码type User struct {
Id int `validate:"number,min=1,max=1000"`
Name string `validate:"string,min=2,max=10"`
Bio string `validate:"string"`
Email string `validate:"email"`
}

validate:"number,min=1,max=1000"就是structTag。如果对这个比较陌生的话,看看下面这个:

1
2
3
4
5
6
7
8
9
复制代码
type User struct {
Id int `json:"id"`
Name string `json:"name"`
Bio string `json:"about,omitempty"`
Active bool `json:"active"`
Admin bool `json:"-"`
CreatedAt time.Time `json:"created_at"`
}

写过golang的基本都用过json:xxx这个用法,json:xxx其实也是一个structTag,只不过这是golang帮你实现好特定用法的structTag。而validate:"number,min=1,max=1000"是我们自定义的structTag。

实现思路

image

我们定义一个接口Validator,定义一个方法Validate。再定义有具体意义的验证器例如StringValidatorNumberValidatorEmailValidator来实现接口Validator
这里为什么要使用接口?假设我们不使用接口代码会怎么写?

1
2
3
4
5
6
7
8
9
复制代码if tagIsOfNumber(){
validator := NumberValidator{}
}else if tagIsOfString() {
validator := StringValidator{}
}else if tagIsOfEmail() {
validator := EmailValidator{}
}else if tagIsOfDefault() {
validator := DefaultValidator{}
}

这样的话判断逻辑不能写在一个函数中,因为返回值validator会因为structTag的不同而不同,而且validator也不能当做函数参数做传递。而我们定义一个接口,所有的validator都去实现这个接口,上述的问题就能解决,而且逻辑更加清晰和紧凑。
关于接口的使用可以看下标准库的io Writer,Writer是个interface,只有一个方法Writer:

1
2
3
复制代码type Writer interface {
Write(p []byte) (n int, err error)
}

而输出函数可以直接调用参数的Write方法即可,无需关心到底是写到文件还是写到标准输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf) //调用Write方法
p.free()
return
}

//调用
Fprintf(os.Stdout, format, a...) //标准输出
Fprintf(os.Stderr, msg+"\n", args...) //标准错误输出

var buf bytes.Buffer
Fprintf(&buf, "[") //写入到Buffer的缓存中

言归正传,我们看下完整代码,代码是Custom struct field tags in Golang中给出的:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
复制代码package main

import (
"fmt"
"reflect"
"regexp"
"strings"
)

const tagName = "validate"

//邮箱验证正则
var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z`)

//验证接口
type Validator interface {
Validate(interface{}) (bool, error)
}

type DefaultValidator struct {
}

func (v DefaultValidator) Validate(val interface{}) (bool, error) {
return true, nil
}

type StringValidator struct {
Min int
Max int
}

func (v StringValidator) Validate(val interface{}) (bool, error) {
l := len(val.(string))

if l == 0 {
return false, fmt.Errorf("cannot be blank")
}

if l < v.Min {
return false, fmt.Errorf("should be at least %v chars long", v.Min)
}

if v.Max >= v.Min && l > v.Max {
return false, fmt.Errorf("should be less than %v chars long", v.Max)
}

return true, nil
}


type NumberValidator struct {
Min int
Max int
}

func (v NumberValidator) Validate(val interface{}) (bool, error) {
num := val.(int)

if num < v.Min {
return false, fmt.Errorf("should be greater than %v", v.Min)
}

if v.Max >= v.Min && num > v.Max {
return false, fmt.Errorf("should be less than %v", v.Max)
}

return true, nil
}

type EmailValidator struct {
}

func (v EmailValidator) Validate(val interface{}) (bool, error) {
if !mailRe.MatchString(val.(string)) {
return false, fmt.Errorf("is not a valid email address")
}
return true, nil
}

func getValidatorFromTag(tag string) Validator {
args := strings.Split(tag, ",")

switch args[0] {
case "number":
validator := NumberValidator{}
//将structTag中的min和max解析到结构体中
fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
return validator
case "string":
validator := StringValidator{}
fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
return validator
case "email":
return EmailValidator{}
}

return DefaultValidator{}
}

func validateStruct(s interface{}) []error {
errs := []error{}

v := reflect.ValueOf(s)

for i := 0; i < v.NumField(); i++ {
//利用反射获取structTag
tag := v.Type().Field(i).Tag.Get(tagName)

if tag == "" || tag == "-" {
continue
}

validator := getValidatorFromTag(tag)

valid, err := validator.Validate(v.Field(i).Interface())
if !valid && err != nil {
errs = append(errs, fmt.Errorf("%s %s", v.Type().Field(i).Name, err.Error()))
}
}

return errs
}

type User struct {
Id int `validate:"number,min=1,max=1000"`
Name string `validate:"string,min=2,max=10"`
Bio string `validate:"string"`
Email string `validate:"email"`
}

func main() {
user := User{
Id: 0,
Name: "superlongstring",
Bio: "",
Email: "foobar",
}

fmt.Println("Errors:")
for i, err := range validateStruct(user) {
fmt.Printf("\t%d. %s\n", i+1, err.Error())
}
}

代码很好理解,结构也很清晰,不做过多解释了^_^

github上其实已经有现成的验证包了govalidator,支持内置支持的验证tag和自定义验证tag:

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
复制代码package main

import (
"github.com/asaskevich/govalidator"
"fmt"
"strings"
)

type Server struct {
ID string `valid:"uuid,required"`
Name string `valid:"machine_id"`
HostIP string `valid:"ip"`
MacAddress string `valid:"mac,required"`
WebAddress string `valid:"url"`
AdminEmail string `valid:"email"`
}

func main() {
server := &Server{
ID: "123e4567-e89b-12d3-a456-426655440000",
Name: "IX01",
HostIP: "127.0.0.1",
MacAddress: "01:23:45:67:89:ab",
WebAddress: "www.example.com",
AdminEmail: "admin@exmaple.com",
}

//自定义tag验证函数
govalidator.TagMap["machine_id"] = govalidator.Validator(func(str string) bool {
return strings.HasPrefix(str, "IX")
})

if ok, err := govalidator.ValidateStruct(server); err != nil {
panic(err)
} else {
fmt.Printf("OK: %v\n", ok)
}
}

参考资料:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%