1. 结构体标签是什么
Go语言结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。
总结一句话:Go语言允许我们通过结构体字段标签给一个字段附加可以被反射获取的”元信息“
例如:
type Person struct {Name string `json:"name"`Age int `json:"age"`
}
例子中,json:"name"和json:"age" 就是结构体 tag。结构体 tag 的使用非常直观。你只需要在定义结构体字段后,通过反引号 `` 包裹起来的键值对形式就可定义它们。
结构体标签通过在字段上添加特定格式的注释,为字段提供了额外的元信息,但如果没有其他代码对其进行检查,结构体标签对你的代码运行没有任何影响。
2. 结构体标签使用规范
结构体标签字符串的值是一个由空格分隔的 key:"value" 对列表,例如:
type Person struct {Name string `json:"name" xml:"name"`
}
键,通常表示后面跟的“值”是被哪个包使用的,例如json这个键会被encoding/json包处理使用。如果要在“键”对应的“值”中传递多个信息,通常通过用逗号(',')分隔来指定,例如
type User struct {Name string `json:"name,omitempty"`
}
按照惯例,如果一个字段的结构体标签里某个键的“值”被设置成了的破折号 ('-'),那么就意味着告诉处理该结构体标签键值的进程排除该字段。例如,把一个字段的标签设置成下面这样
type User struct {Name string `json:"-"`
}
这样进行JSON编码/解码时忽略Name这个字段。
3. 如何获取到结构体标签
结构体标签是给反射准备的
结构体字段类型相关的信息,在反射中是reflect.StructFiled这个类型表示的。
type StructField struct {Name stringType Type // field typeTag StructTag // field tag string......
}
其中包含的Tag字段是一个StructTag 类型的值,代表了字段声明中的结构体标签信息。
而reflect.StructField可以通过reflect.Type提供的下面两种方式拿到
// 根据 index 获取结构体内字段的类型对象
Field(i int) StructField
// 根据字段名获取结构体内字段的类型对象
FieldByName(name string) (StructField, bool)
我们可以使用可以使用StructTag的Get方法解析标签的值并返回你指定的键的“值”。
func (tag StructTag) Get(key string) string
为了方便判断一个给定的key是否存在与标签中,StructTag还提供了一个Lookup方法
func (tag StructTag) Lookup(key string) (value string, ok bool)
跟Get方法不同的是,Lookup会通过返回的ok值告知给定key是否存在与标签中。
下面通过一个例子,演示下获取我们自定义标签的过程。
package mainimport ("fmt""reflect"
)type Person struct {Name string `myTag:"MyName" json:"name"`Age int32 `myTag:"MyAge" json:"age"`Job string `myTag:"MyJob" json:"job,omitempty"`Address string `myTag:"MyAddress" json:"-"`
}func main() {p := Person{"Liu", 28, "Stu", "HZ"}t := reflect.TypeOf(p)for i := 0; i < t.NumField(); i++ {field := t.Field(i)fmt.Printf("Field: Person.%s\n", field.Name)fmt.Printf("\tWhole tag value : %s\n", field.Tag)fmt.Printf("\tValue of 'json': %s\n", field.Tag.Get("json"))fmt.Printf("\tValue of 'myTag': %s\n", field.Tag.Get("myTag"))}
}
结果:
Field: Person.NameWhole tag value : myTag:"MyName" json:"name"Value of 'json': nameValue of 'myTag': MyName
Field: Person.AgeWhole tag value : myTag:"MyAge" json:"age"Value of 'json': ageValue of 'myTag': MyAge
Field: Person.JobWhole tag value : myTag:"MyJob" json:"job,omitempty"Value of 'json': job,omitemptyValue of 'myTag': MyJob
Field: Person.AddressWhole tag value : myTag:"MyAddress" json:"-"Value of 'json': -Value of 'myTag': MyAddress
4. 自定义标签使用
4.1 结构体字段验证
下面通过一个使用自定义标签校验结构体字段的例子来看一下自定义标签的使用:
type Person struct {Name string `validate:"required"`Age int32 `validate:"min=18"`
}
在这个示例中,我们有一个名为 Person 的结构体。该结构体有一个叫做 "Age" 的字段,该字段有一个结构标签 min=18,将最小年龄限制设定为 18 岁。
该结构体还有一个名为 "Name" 的字段,该字段有一个结构标签 validate:"required",这意味着该字段不能为空。
例子的目标是创建一个可以理解这些结构标签并基于这些规则验证Person对象是否有效的验证函数。具体来说,我们将创建一个 Validate(any) 函数,用来检查Person的年龄是否≥18岁,以及姓名字段是否不为空。
package mainimport ("fmt""reflect""strconv""strings"
)type Person struct {Name string `validate:"required"`Age int32 `validate:"min=18"`
}func Validate(s interface{}) error {val := reflect.Indirect(reflect.ValueOf(s))for i := 0; i < val.NumField(); i++ {valueField := val.Field(i)typeField := val.Type().Field(i)tag := typeField.Tag.Get("validate")if tag == "" {continue}fmt.Println(tag)// split the tag so we can use like this: `required:"limit=18"rules := strings.Split(tag, ",")for _, rule := range rules {parts := strings.Split(rule, "=")key := parts[0]var value stringif len(parts) > 1 {value = parts[1]}switch key {case "required":if err := isRequired(valueField); err != nil {return err}case "min":if err := isMin(valueField, value); err != nil {return err}}}}return nil
}
func isMin(valueField reflect.Value, minStr string) error {typeField := valueField.Type()if minStr == "" {return nil}minAge, err := strconv.ParseFloat(minStr, 64)if err != nil {return fmt.Errorf("minAge value %f is not a number", minAge)}switch valueField.Kind() {case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:if float64(valueField.Int()) < minAge {return fmt.Errorf("field %s must be greater or equal %d", typeField.Name(), int(minAge))}case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:if float64(valueField.Uint()) < minAge {return fmt.Errorf("field %s must be greater or equal than %d", typeField.Name(), uint(minAge))}case reflect.Float32, reflect.Float64:if valueField.Float() < minAge {return fmt.Errorf("field %s must be greater or equal than %f", typeField.Name(), minAge)}default:return fmt.Errorf("unhandled default case")}return nil
}func isRequired(v reflect.Value) error {switch v.Kind() {case reflect.Array, reflect.Map, reflect.Slice, reflect.String:if v.Len() != 0 {return nil}case reflect.Bool:if v.Bool() {return nil}case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:if v.Int() != 0 {return nil}case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:if v.Uint() != 0 {return nil}default:return fmt.Errorf("unhandled default case")}return fmt.Errorf("field %s is required", v.Type().Name())
}
func main() {p := Person{"Liu", 17}err := Validate(p)if err != nil {fmt.Println(err)}
}
遍历 Person 结构体的所有字段,以便检索结构标签 “validate” 的值,获取标签 “validate” 的值后进行处理
结果:
required
min=18
field int32 must be greater or equal 18
不过当我们涉及到验证时,没必要自己造轮子,而是要首先考虑使用现有的验证包,因为它们通常具有更广泛的功能,并且更好地处理边缘情况。
例如 "validator"包,它具有一系列内置函数,如 required 和 min,可以在验证过程中使用
4.2 结构体字段访问控制
根据用户的角色(如 admin、user)或者请求的来源(admin、web)控制对结构体字段的访问。具体而言,假设我定义了一个包含敏感信息的结构体,我可以使用自定义 tag 来标记每个字段的访问权限。
package mainimport ("fmt""reflect""strings"
)type UserProfile struct {Username string `access:"user"` // 所有用户可见Email string `access:"user"` // 所有用户可见PhoneNumber string `access:"admin"` // 仅管理员可见Address string `access:"admin"` // 仅管理员可见
}func FilterFieldsByRole(profile UserProfile, role string) map[string]string {result := make(map[string]string)val := reflect.ValueOf(profile)typ := val.Type()for i := 0; i < val.NumField(); i++ {field := typ.Field(i)accessTag := field.Tag.Get("access")if accessTag == "user" || accessTag == role {// 获取字段名称fieldName := strings.ToLower(field.Name)// 获取字段值fieldValue := val.Field(i).String()// 组织返回结果 resultresult[fieldName] = fieldValue}}return result
}
func main() {profile := UserProfile{Username: "Liu",Email: "Liu@example.com",PhoneNumber: "123-456-78900",Address: "HZ",}// 假设当前用户是普通用户userInfo := FilterFieldsByRole(profile, "user")fmt.Println("普通用户:", userInfo)// 假设当前用户是管理员adminInfo := FilterFieldsByRole(profile, "admin")fmt.Println("管理员:", adminInfo)
}
根据 UserProfile 定义的 access tag 决定字段内容的可见性。
根据角色来控制字段函数为 FilterFieldsByRole,它接受一个 UserProfile 类型变量和用户角色,返回内容一个过滤后的 map(由 fieldname 到 fieldvalue 组成的映射),其中只包含角色有权访问的字段。
限控制的重点逻辑部分,就是 if accessTag == "user" || accessTag == role 这段判断条件。当满足条件之后,接下来要做的就是通过反射获取字段名称和值,并组织目标的 Map 类变量 result了。
结果:
普通用户: map[email:Liu@example.com username:Liu]
管理员: map[address:HZ email:Liu@example.com phonenumber:123-456-78900 username:Liu]
5. 常用的结构体标签键
常用的结构体标签Key,指的是那些被一些常用的开源包声明使用的结构体标签键。在这里总结了一些,都是一些我们平时会用到的包,它们是:
-
json: 由encoding/json包使用,详见json.Marshal()的使用方法和实现逻辑。 -
xml: 由encoding/xml包使用,详见xml.Marshal()。 -
bson: 由gobson包,和mongo-go包使用。 -
protobuf: 由github.com/golang/protobuf/proto使用,在包文档中有详细说明。 -
yaml: 由gopkg.in/yaml.v2包使用,详见yaml.Marshal()。 -
gorm: 由gorm.io/gorm包使用,示例可以在GORM的文档中找到。
