golang 如何驗證struct字段的數(shù)據(jù)格式

golang 如何驗證struct字段的數(shù)據(jù)格式

網(wǎng)上看到一個字段驗證的方法記錄一下方便以后再用
原文地址http://mp.weixin.qq.com/s/4Md7yWFyZLYKp68snsFHbw
假設(shè)我們有如下結(jié)構(gòu)體:

type User struct {
    Id    int    
    Name  string 
    Bio   string 
    Email string 
}

我們需要對結(jié)構(gòu)體內(nèi)的字段進(jìn)行驗證合法性:

? Id的值在某一個范圍內(nèi)宠互。
? Name的長度在某一個范圍內(nèi)箱蟆。
? 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
}

這樣的話代碼比較冗余呀非,而且如果結(jié)構(gòu)體新加字段,還需要再修改驗證函數(shù)再加一段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幫你實現(xiàn)好特定用法的structTag琢锋。而validate:"number,min=1,max=1000"是我們自定義的structTag辕漂。

實現(xiàn)思路

我們定義一個接口Validator,定義一個方法Validate吴超。再定義有具體意義的驗證器例如StringValidator钉嘹、NumberValidator、EmailValidator來實現(xiàn)接口Validator鲸阻。

這里為什么要使用接口跋涣?假設(shè)我們不使用接口代碼會怎么寫?

if tagIsOfNumber(){
        validator := NumberValidator{}
}else if tagIsOfString() {
        validator := StringValidator{}
}else if tagIsOfEmail() {
        validator := EmailValidator{}
}else if tagIsOfDefault() {
        validator := DefaultValidator{}
}

這樣的話判斷邏輯不能寫在一個函數(shù)中鸟悴,因為返回值validator會因為structTag的不同而不同陈辱,而且validator也不能當(dāng)做函數(shù)參數(shù)做傳遞。而我們定義一個接口细诸,所有的validator都去實現(xiàn)這個接口沛贪,上述的問題就能解決,而且邏輯更加清晰和緊湊。

關(guān)于接口的使用可以看下標(biāo)準(zhǔn)庫的io Writer利赋,Writer是個interface水评,只有一個方法Writer:

type Writer interface {
    Write(p []byte) (n int, err error)
}

而輸出函數(shù)可以直接調(diào)用參數(shù)的Write方法即可,無需關(guān)心到底是寫到文件還是寫到標(biāo)準(zhǔn)輸出:

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)      //調(diào)用Write方法
    p.free()
    return
}

//調(diào)用
Fprintf(os.Stdout, format, a...) //標(biāo)準(zhǔn)輸出
Fprintf(os.Stderr, msg+"\n", args...) //標(biāo)準(zhǔn)錯誤輸出

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解析到結(jié)構(gòu)體中
        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())
    }
}

代碼很好理解,結(jié)構(gòu)也很清晰塘偎,不做過多解釋了_

github上其實已經(jīng)有現(xiàn)成的驗證包了govalidator褪那,支持內(nèi)置支持的驗證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驗證函數(shù)
    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)
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市式塌,隨后出現(xiàn)的幾起案子博敬,更是在濱河造成了極大的恐慌,老刑警劉巖峰尝,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偏窝,死亡現(xiàn)場離奇詭異,居然都是意外死亡武学,警方通過查閱死者的電腦和手機(jī)祭往,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來火窒,“玉大人硼补,你說我怎么就攤上這事⊙螅” “怎么了已骇?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長褪储。 經(jīng)常有香客問我,道長鲤竹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任辛藻,我火速辦了婚禮,結(jié)果婚禮上互订,老公的妹妹穿的比我還像新娘吱肌。我一直安慰自己,他們只是感情好岩榆,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布坟瓢。 她就那樣靜靜地躺著勇边,像睡著了一般。 火紅的嫁衣襯著肌膚如雪折联。 梳的紋絲不亂的頭發(fā)上粒褒,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機(jī)與錄音奕坟,去河邊找鬼。 笑死月杉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的苛萎。 我是一名探鬼主播检号,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼齐苛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起凹蜂,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎泥彤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吟吝,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡颈娜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了官辽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡萤捆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出俗或,到底是詐尸還是另有隱情,我是刑警寧澤辛慰,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站驰弄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏戚篙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一已球、第九天 我趴在偏房一處隱蔽的房頂上張望辅愿。 院中可真熱鬧智亮,春花似錦、人聲如沸点待。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽苗踪。三九已至,卻和暖如春通铲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背颅夺。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工吧黄, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留部服,地道東北人拗慨。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓奉芦,卻偏偏與公主長得像剧蹂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子国夜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 工廠模式類似于現(xiàn)實生活中的工廠可以產(chǎn)生大量相似的商品车吹,去做同樣的事情,實現(xiàn)同樣的效果;這時候需要使用工廠模式窄驹。簡單...
    舟漁行舟閱讀 7,758評論 2 17
  • HTML表單 在HTML中证逻,表單是 ... 之間元素的集合,它們允許訪問者輸入文本囚企、選擇選項、操作對象等等龙宏,然后將...
    蘭山小亭閱讀 3,417評論 2 14
  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,697評論 0 3
  • 好怕大家想的東西都一樣银酗,這樣世界只會越來越?jīng)]意思了辆影。
    月轉(zhuǎn)長廊閱讀 140評論 0 0
  • 寫在前面 昨天看到簡友@提燈忘月的文黍特,《實名反對“輸入輸出”論》。說很多人現(xiàn)在把“讀書”當(dāng)輸入灭衷,把“寫作”當(dāng)輸出的...
    林對對閱讀 529評論 1 5