原文连接: https://sosedoff.com/2016/07/16/golang-struct-tags.htmlhtml
struct是golang中最常使用的变量类型之一,几乎每一个地方都有使用,从处理配置选项到使用encoding/json或encoding/xml包编排JSON或XML文档。字段标签是struct字段定义部分,容许你使用优雅简单的方式存储许多用例字段的元数据(如字段映射,数据校验,对象关系映射等等)。golang
一般structs最让人感兴趣的是什么?strcut最有用的特征之一是可以制定字段名映射。若是你处理外部服务并进行大量数据转换它将很是方便。让咱们看下以下示例:数据库
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结构体中,标签仅仅是字段类型定义后面用反引号封闭的字符串。在示例中咱们从新定义字段名以便进行JSON编码和反编码。意即当对结构体字段进行JSON编码,它将会使用用户定义的字段名代替默认的大写名字。下面是经过json.Marshal调用产生的没有自定义标签的结构体输出:express
{ "Id": 1, "Name": "John Doe", "Bio": "Some Text", "Active": true, "Admin": false, "CreatedAt": "2016-07-16T15:32:17.957714799Z" }
如你所见,示例中全部的字段输出都与它们在User结构体中定义相关。如今,让咱们添加自定义JSON标签,看会发生什么:json
{ "id": 1, "name": "John Doe", "about": "Some Text", "active": true, "created_at": "2016-07-16T15:32:17.957714799Z" }
经过自定义标签咱们可以重塑输出。使用json:"-"定义咱们告诉编码器彻底跳过该字段。查看JSON和XML包以获取更多细节和可用的标签选项。app
既然咱们理解告终构体标签是如何被定义和使用的,咱们尝试编写本身的标签处理器。为实现该功能咱们须要检查结构体而且读取标签属性。这就须要用到reflect包。less
假定咱们要实现简单的校验库,基于字段类型使用字段标签订义一些校验规则。咱们常想要在将数据保存到数据库以前对其进行校验。ui
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'
经过reflect包咱们可以获取User结构体id基本信息,包括它的类型、种类且能列出它的全部字段。如你所见,咱们打印了每一个字段的标签。标签没有什么神奇的地方,field.Tag.Get方法返回与标签名匹配的字符串,你能够自由使用作你想作的。code
为向你说明如何使用结构体标签进行校验,我使用接口形式实现了一些校验类型(numeric, string, email).下面是可运行的代码示例:
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结构体咱们定义了一个Id字段校验规则,检查值是否在合适范围1-1000之间。Name字段值是一个字符串,校验器应检查其长度。Bio字段值是一个字符串,咱们仅需其值不为空,不须校验。最后,Email字段值应是一个合法的邮箱地址(至少是格式化的邮箱)。例中User结构体字段均非法,运行代码将会得到如下输出:
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()获取字段值,该方法提供了一个接口,咱们能够进行校验。使用v.Type().Filed(i)咱们还能够获取字段类型。