golang 如何验证struct字段的数据格式
本文同时发表在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) } 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:
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) } }
参考资料:
Custom struct field tags in Golang Data validation in Golang govalidator上一篇: Lua脚本语言概述
下一篇: 浏览器根对象window之screen