本文同时发表在https://github.com/zhangyachen/zhangyachen.github.io/issues/125
假设我们有如下结构体:
type User struct {
Id int
Name string
Bio string
Email string
}
我们需要对结构体内的字段进行验证合法性:
- Id的值在某一个范围内。
- Name的长度在某一个范围内。
- Email格式正确。
我们可能会这么写:
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来解决上述的问题:
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。如果对这个比较陌生的话,看看下面这个:
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。
实现思路
我们定义一个接口Validator
,定义一个方法Validate
。再定义有具体意义的验证器例如StringValidator
、NumberValidator
、EmailValidator
来实现接口Validator
。
这里为什么要使用接口?假设我们不使用接口代码会怎么写?
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:
type Writer interface {
Write(p []byte) (n int, err error)
}
而输出函数可以直接调用参数的Write方法即可,无需关心到底是写到文件还是写到标准输出:
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中给出的:
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