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

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

网上看到一个字段验证的方法记录一下方便以后再用
原文地址http://mp.weixin.qq.com/s/4Md7yWFyZLYKp68snsFHbw
假设我们有如下结构体:

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, "[") //
言归正传,我们看下完整代码,代码是 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)
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容

  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,729评论 2 17
  • HTML表单 在HTML中,表单是 ... 之间元素的集合,它们允许访问者输入文本、选择选项、操作对象等等,然后将...
    兰山小亭阅读 3,413评论 2 14
  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,683评论 0 3
  • 好怕大家想的东西都一样,这样世界只会越来越没意思了。
    月转长廊阅读 137评论 0 0
  • 写在前面 昨天看到简友@提灯忘月的文,《实名反对“输入输出”论》。说很多人现在把“读书”当输入,把“写作”当输出的...
    林对对阅读 518评论 1 5