golang自定義struct字段標(biāo)簽

原文鏈接: https://sosedoff.com/2016/07/16/golang-struct-tags.html

struct是golang中最常使用的變量類型之一属百,幾乎每個(gè)地方都有使用捣郊,從處理配置選項(xiàng)到使用encoding/json或encoding/xml包編排JSON或XML文檔黎炉。字段標(biāo)簽是struct字段定義部分,允許你使用優(yōu)雅簡(jiǎn)單的方式存儲(chǔ)許多用例字段的元數(shù)據(jù)(如字段映射玲躯,數(shù)據(jù)校驗(yàn)掌呜,對(duì)象關(guān)系映射等等)皿桑。

基本原理

通常structs最讓人感興趣的是什么行瑞?strcut最有用的特征之一是能夠制定字段名映射。如果你處理外部服務(wù)并進(jìn)行大量數(shù)據(jù)轉(zhuǎn)換它將非常方便侥衬。讓我們看下如下示例:

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"`
}

在User結(jié)構(gòu)體中诗祸,標(biāo)簽僅僅是字段類型定義后面用反引號(hào)封閉的字符串。在示例中我們重新定義字段名以便進(jìn)行JSON編碼和反編碼轴总。意即當(dāng)對(duì)結(jié)構(gòu)體字段進(jìn)行JSON編碼直颅,它將會(huì)使用用戶定義的字段名代替默認(rèn)的大寫名字。下面是通過(guò)json.Marshal調(diào)用產(chǎn)生的沒(méi)有自定義標(biāo)簽的結(jié)構(gòu)體輸出:

{
  "Id": 1,
  "Name": "John Doe",
  "Bio": "Some Text",
  "Active": true,
  "Admin": false,
  "CreatedAt": "2016-07-16T15:32:17.957714799Z"
}

如你所見(jiàn)怀樟,示例中所有的字段輸出都與它們?cè)赨ser結(jié)構(gòu)體中定義相關(guān)」Τィ現(xiàn)在,讓我們添加自定義JSON標(biāo)簽漂佩,看會(huì)發(fā)生什么:

{
  "id": 1,
  "name": "John Doe",
  "about": "Some Text",
  "active": true,
  "created_at": "2016-07-16T15:32:17.957714799Z"
}

通過(guò)自定義標(biāo)簽我們能夠重塑輸出。使用json:"-"定義我們告訴編碼器完全跳過(guò)該字段罪塔。查看JSON和XML包以獲取更多細(xì)節(jié)和可用的標(biāo)簽選項(xiàng)投蝉。

自主研發(fā)

既然我們理解了結(jié)構(gòu)體標(biāo)簽是如何被定義和使用的,我們嘗試編寫自己的標(biāo)簽處理器征堪。為實(shí)現(xiàn)該功能我們需要檢查結(jié)構(gòu)體并且讀取標(biāo)簽屬性瘩缆。這就需要用到reflect包。

假定我們要實(shí)現(xiàn)簡(jiǎn)單的校驗(yàn)庫(kù)佃蚜,基于字段類型使用字段標(biāo)簽定義一些校驗(yàn)規(guī)則庸娱。我們常想要在將數(shù)據(jù)保存到數(shù)據(jù)庫(kù)之前對(duì)其進(jìn)行校驗(yàn)着绊。

package main

import (
    "reflect"
    "fmt"
)

const tagName = "validate"

type User struct {
    Id int `validate:"-"`
    Name string `validate:"presence,min=2,max=32"`
    Email string `validate:"email,required"`
}

func main() {
    user := User{
        Id: 1,
        Name: "John Doe",
        Email: "john@example",
    }

    // TypeOf returns the reflection Type that represents the dynamic type of variable.
    // If variable is a nil interface value, TypeOf returns nil.
    t := reflect.TypeOf(user)

    //Get the type and kind of our user variable
    fmt.Println("Type: ", t.Name())
    fmt.Println("Kind: ", t.Kind())

    for i := 0; i < t.NumField(); i++ {
        // Get the field, returns https://golang.org/pkg/reflect/#StructField
        field := t.Field(i)

        //Get the field tag value
        tag := field.Tag.Get(tagName)

        fmt.Printf("%d. %v(%v), tag:'%v'\n", i+1, field.Name, field.Type.Name(), tag)
    }


}

輸出:

Type:  User
Kind:  struct
1. Id(int), tag:'-'
2. Name(string), tag:'presence,min=2,max=32'
3. Email(string), tag:'email,required'

通過(guò)reflect包我們能夠獲取User結(jié)構(gòu)體id基本信息,包括它的類型熟尉、種類且能列出它的所有字段归露。如你所見(jiàn),我們打印了每個(gè)字段的標(biāo)簽斤儿。標(biāo)簽沒(méi)有什么神奇的地方剧包,field.Tag.Get方法返回與標(biāo)簽名匹配的字符串,你可以自由使用做你想做的往果。

為向你說(shuō)明如何使用結(jié)構(gòu)體標(biāo)簽進(jìn)行校驗(yàn)疆液,我使用接口形式實(shí)現(xiàn)了一些校驗(yàn)類型(numeric, string, email).下面是可運(yùn)行的代碼示例:

package main

import (
    "regexp"
    "fmt"
    "strings"
    "reflect"
)

//Name of the struct tag used in example.
const tagName = "validate"

//Regular expression to validate email address.
var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z`)

//Generic data validator
type Validator interface {
    //Validate method performs validation and returns results and optional error.
    Validate(interface{})(bool, error)
}

//DefaultValidator does not perform any validations
type DefaultValidator struct{

}

func (v DefaultValidator) Validate(val interface{}) (bool, error) {
    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
}

//StringValidator validates string presence and/or its length
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 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
}

//Returns validator struct corresponding to validation type
func getValidatorFromTag(tag string) Validator {
    args := strings.Split(tag, ",")

    switch args[0] {
    case "number":
        validator := NumberValidator{}
        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{}
}

//Performs actual data validation using validator definitions on the struct
func validateStruct(s interface{}) []error {
    errs := []error{}

    //ValueOf returns a Value representing the run-time data
    v := reflect.ValueOf(s)

    for i := 0; i < v.NumField(); i++ {
        //Get the field tag value
        tag := v.Type().Field(i).Tag.Get(tagName)

        //Skip if tag is not defined or ignored
        if tag == "" || tag == "-" {
            continue
        }

        //Get a validator that corresponds to a tag
        validator := getValidatorFromTag(tag)

        //Perform validation
        valid, err := validator.Validate(v.Field(i).Interface())

        //Append error to results
        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:"string"`
}

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())
    }
}

輸出:

Errors: 
    1. Id should be greater than 1
    2. Name should be less than 10 chars long
    3. Bio cannot be blank
    4. Email should be less than 0 chars long
    

在User結(jié)構(gòu)體我們定義了一個(gè)Id字段校驗(yàn)規(guī)則,檢查值是否在合適范圍1-1000之間陕贮。Name字段值是一個(gè)字符串堕油,校驗(yàn)器應(yīng)檢查其長(zhǎng)度。Bio字段值是一個(gè)字符串肮之,我們僅需其值不為空掉缺,不須校驗(yàn)。最后局骤,Email字段值應(yīng)是一個(gè)合法的郵箱地址(至少是格式化的郵箱)攀圈。例中User結(jié)構(gòu)體字段均非法,運(yùn)行代碼將會(huì)獲得以下輸出:

Errors: 
    1. Id should be greater than 1
    2. Name should be less than 10 chars long
    3. Bio cannot be blank
    4. Email should be less than 0 chars long
    

最后一例與之前例子(使用類型的基本反射)的主要不同之處在于峦甩,我們使用reflect.ValueOf代替reflect.TypeOf赘来。還需要使用v.Field(i).Interface()獲取字段值,該方法提供了一個(gè)接口凯傲,我們可以進(jìn)行校驗(yàn)犬辰。使用v.Type().Filed(i)我們還可以獲取字段類型。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末冰单,一起剝皮案震驚了整個(gè)濱河市幌缝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诫欠,老刑警劉巖涵卵,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異荒叼,居然都是意外死亡轿偎,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門被廓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)坏晦,“玉大人,你說(shuō)我怎么就攤上這事±バ觯” “怎么了球碉?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)仓蛆。 經(jīng)常有香客問(wèn)我睁冬,道長(zhǎng),這世上最難降的妖魔是什么多律? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任痴突,我火速辦了婚禮,結(jié)果婚禮上狼荞,老公的妹妹穿的比我還像新娘辽装。我一直安慰自己,他們只是感情好相味,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布拾积。 她就那樣靜靜地躺著,像睡著了一般丰涉。 火紅的嫁衣襯著肌膚如雪拓巧。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天一死,我揣著相機(jī)與錄音肛度,去河邊找鬼。 笑死投慈,一個(gè)胖子當(dāng)著我的面吹牛承耿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播伪煤,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼加袋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了抱既?” 一聲冷哼從身側(cè)響起职烧,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎防泵,沒(méi)想到半個(gè)月后蚀之,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捷泞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年足删,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肚邢。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡壹堰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出骡湖,到底是詐尸還是另有隱情贱纠,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布响蕴,位于F島的核電站谆焊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏浦夷。R本人自食惡果不足惜辖试,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望劈狐。 院中可真熱鬧罐孝,春花似錦、人聲如沸肥缔。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)续膳。三九已至改艇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間坟岔,已是汗流浹背谒兄。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留社付,地道東北人承疲。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像瘦穆,于是被迫代替她去往敵國(guó)和親纪隙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

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