zoukankan      html  css  js  c++  java
  • 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。

    实现思路

    image

    我们定义一个接口Validator,定义一个方法Validate。再定义有具体意义的验证器例如StringValidatorNumberValidatorEmailValidator来实现接口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+"
    ", 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-zd-]+(.[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("	%d. %s
    ", 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
    ", ok)
        }
    }
    

    参考资料:

    朋友们可以关注下我的公众号,获得最及时的更新:

    image

  • 相关阅读:
    ms日期函数大全
    Sql server char,nchar,varchar与Nvarchar的区别
    clientX pageX
    jswindow对象的方法和属性资料
    Asp.Net中不修改IIS实现URL重写,支持任意扩展名及无扩展名
    该行已经属于另一个表的解决方法
    dopostBack机制(转)
    js触发asp.net的Button的Onclick事件
    Value、ReadString、ReadContentAsString、ReadElementContentAsString 区别
    有关元素元素位置的属性
  • 原文地址:https://www.cnblogs.com/zhangyachen/p/8030245.html
Copyright © 2011-2022 走看看