原文鏈接: 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)我們還可以獲取字段類型。