#golang #struct-tag #reflect


StructTag是写在结构体字段类型后面反引号中的内容,用来标记结构体中各字段的属性。

源码中对struct tag的解释:

By convention, tag strings are a concatenation of optionally space-separated key:“value” pairs. Each key is a non-empty string consisting of non-control characters other than space (U+0020 ’ ‘), quote (U+0022 ‘"’), and colon (U+003A ‘:’). Each value is quoted using U+0022 ‘"’ characters and Go string literal syntax.

简单应用

最常见的,比如jsontag应用:

json序列化和反序列化时候使用的key都是在struct字段上定义的

package main

import (
	"encoding/json"
	"fmt"
)

type User struct {
	ID       int    `json:"id"`
	Username string `json:"username"`
	Age      int    `json:"age"`
	Email    string `json:"email"`
}

func main() {
	u := User{
		ID:       1,
		Username: "ormissia",
		Age:      90,
		Email:    "email@example.com",
	}

	userJson, _ := json.Marshal(u)
	fmt.Println(string(userJson))

	u2Str := `{"id":2,"username":"ormissia","age":900,"email":"ormissia@example.com"}`
	u2 := new(User)
	_ = json.Unmarshal([]byte(u2Str), u2)
	fmt.Printf("%+v",u2)
}

输出:

{"id":1,"username":"ormissia","age":90,"email":"email@example.com"}
&{ID:2 Username:ormissia Age:900 Email:ormissia@example.com}

tag解析原理

通过反射拿到struct tag

示例:

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	ID       int    `json:"id" myTag:"ID"`
	Username string `json:"username" myTag:"USERNAME"`
	Age      int    `json:"age" myTag:"AGE"`
	Email    string `json:"email" myTag:"EMAIL"`
}

func main() {
	u := User{1, "ormissia", 90, "email@example.com"}

	userTyp := reflect.TypeOf(u)
	fieldTag := userTyp.Field(0).Tag
	fmt.Printf("user field 0 id tag: %s\n",fieldTag)
	value, ok1 := fieldTag.Lookup("myTag")
	fmt.Println(value, ok1)
	value1, ok2 := fieldTag.Lookup("other")
	fmt.Println(value1, ok2)
}

输出:

user field 0 id tag: json:"id" myTag:"ID"
ID true
 false

获取tag全部的值

reflect.TypeOf(u).Field(0).Tag

通过Tag即可获取struct定义时候对应字段后面反引号``中全部的值

Tag是通过反射获取到的具体字段StructField 中的属性,类型为自定义string类型:StructTag

type StructField struct {
	// Name is the field name.
	Name string
	// PkgPath is the package path that qualifies a lower case (unexported)
	// field name. It is empty for upper case (exported) field names.
	// See https://golang.org/ref/spec#Uniqueness_of_identifiers
	PkgPath string

	Type      Type      // field type
	Tag       StructTag // field tag string
	Offset    uintptr   // offset within struct, in bytes
	Index     []int     // index sequence for Type.FieldByIndex
	Anonymous bool      // is an embedded field
}
// A StructTag is the tag string in a struct field.
//
// By convention, tag strings are a concatenation of
// optionally space-separated key:"value" pairs.
// Each key is a non-empty string consisting of non-control
// characters other than space (U+0020 ' '), quote (U+0022 '"'),
// and colon (U+003A ':').  Each value is quoted using U+0022 '"'
// characters and Go string literal syntax.
type StructTag string

通过tag的key获取value

StructTag 有两个通过key获取value的方法: GetLookup

Get 是对Lookup 的一个封装。

Lookup 可以返回当前查询的key是否存在。

func (tag StructTag) Get(key string) string {
	v, _ := tag.Lookup(key)
	return v
}
func (tag StructTag) Lookup(key string) (value string, ok bool) {
	for tag != "" {
		// Skip leading space.
		i := 0
		for i < len(tag) && tag[i] == ' ' {
			i++
		}
		tag = tag[i:]
		if tag == "" {
			break
		}

		i = 0
		for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f {
			i++
		}
		if i == 0 || i+1 >= len(tag) || tag[i] != ':' || tag[i+1] != '"' {
			break
		}
		name := string(tag[:i])
		tag = tag[i+1:]

        //...
        //...
}

通过源码,我们可以看出 Lookup 实际上是对tag反引号中整个内容进行查找,通过空格、冒号以及双引号对tag的值进行分割,最后返回。

使用自定义tag实践

我们可以一个struct参数校验器:go-opv 来简单体验一下自定义tag的使用。

go-opv的简单使用示例

go-opv 的简介可以参考我的 另一篇文章 。 只不过,当时还没有添加这个通过struct自定义tag的校验方式,基础功能与现在基本一致。

仓库go-opv

以下是一个简单的使用Demo

在这个示例中,我们指定了structtaggo-opv:"ge:0,le:20",在参数检验过程中,我们会解析这个tag,并从中获取定义的规则。

package main

import (
	"log"

	go_opv "github.com/ormissia/go-opv"
)

type User struct {
	Name string `go-opv:"ge:0,le:20"`  //Name >=0 && Name <=20
	Age  int    `go-opv:"ge:0,lt:100"` //Age >= 0 && Age < 100
}

func init() {
	//使用默认配置:struct tag名字为"go-opv",规则与限定值的分隔符为":"
	myVerifier = go_opv.NewVerifier()
	//初始化一个验证规则:Age字段大于等于0,小于200
	userRequestRules = go_opv.Rules{
		"Age": []string{myVerifier.Ge("0"), myVerifier.Lt("200")},
	}
}

var myVerifier go_opv.Verifier
var userRequestRules go_opv.Rules

func main() {
	// ShouldBind(&user) in Gin framework or other generated object
	user := User{
		Name: "ormissia",
		Age:  190,
	}

	//两种验证方式混合,函数参数中传入自定义规则时候会覆盖struct tag上定义的规则
	//根据自定义规则Age >= 0 && Age < 200,Age的值为190,符合规则,验证通过
	if err := myVerifier.Verify(user, userRequestRules); err != nil {
		log.Println(err)
	} else {
		log.Println("pass")
	}

	//只用struct的tag验证
	//根据tag上定义的规则Age >= 0 && Age < 100,Age的值为190,不符合规则,验证不通过
	if err := myVerifier.Verify(user); err != nil {
		log.Println(err)
	} else {
		log.Println("pass")
	}
}

go-opv的简单分析

我们先判断了是否传入了自定义的校验规则(即为自定义规则会覆盖structtag定义的规则),如果没有,就去通过反射获取structtag定义的规则。 然后生成相对应的规则,继续执行后面的校验逻辑。

if len(conditions[tagVal.Name]) == 0 {
	//没有自定义使用tag
	//`go-opv:"ne:0,eq:10"`
	//conditionsStr = "ne:0,eq:10"
	if conditionsStr, ok := tagVal.Tag.Lookup(verifier.tagPrefix); ok && conditionsStr != "" {
		conditionStrs := strings.Split(conditionsStr, ",")
		conditions[tagVal.Name] = conditionStrs
	} else {
		//如果tag也没有定义则去校验下一个字段
		continue
	}
}

小结

参考