golang⾃定义struct字段标签
struct是golang中最常使⽤的变量类型之⼀,⼏乎每个地⽅都有使⽤,从处理配置选项到使⽤encoding/json或encoding/xml包编排JSON或XML⽂档。字段标签是struct字段定义部分,允许你使⽤优雅简单的⽅式存储许多⽤例字段的元数据(如字段映射,数据校验,对象关系映射等等)。
基本原理
通常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调⽤产⽣的没有⾃定义标签的结构体输出:
{
"Id": 1,
"Name": "John Doe",
"Bio": "Some Text",
"Active": true,
"Admin": false,
"CreatedAt": "2016-07-16T15:32:17.957714799Z"
}
如你所见,⽰例中所有的字段输出都与它们在User结构体中定义相关。现在,让我们添加⾃定义JSON标签,看会发⽣什么:
{
"id": 1,
"name": "John Doe",
"about": "Some Text",
"active": true,
"created_at": "2016-07-16T15:32:17.957714799Z"
}
通过⾃定义标签我们能够重塑输出。使⽤json:"-"定义我们告诉编码器完全跳过该字段。查看JSON和XML包以获取更多细节和可⽤的标签选项。
⾃主研发
既然我们理解了结构体标签是如何被定义和使⽤的,我们尝试编写⾃⼰的标签处理器。为实现该功能我们需要检查结构体并且读取标签属性。这就需要⽤到reflect包。
假定我们要实现简单的校验库,基于字段类型使⽤字段标签定义⼀些校验规则。我们常想要在将数据保存到数据库之前对其进⾏校验。
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 /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⽅法返回与标签名匹配的字符串,你可以⾃由使⽤做你想做的。
为向你说明如何使⽤结构体标签进⾏校验,我使⽤接⼝形式实现了⼀些校验类型(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)我们还可以获取字段类型。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论