GO语言入门-反射5(结构体的Tag)

发布于:2025-04-14 ⋅ 阅读:(26) ⋅ 点赞:(0)

12.5 结构体的 Tag

在定义结构体类型时,可以在字段后面加上一个字符串,称为 Struct Tag。Tag 主要用来补充附加信息。

Tag 由多个 key - value 构成,并以空格来分隔,key 和 value 之间用英文的冒号分隔。其格式如下:

key1:"value1" key2:"value2" key3:"value3"……

value 必须放在一对双引号中。为了避免字符转义,Tag 字符串可以用 “`” 字符来包装,就像下面这样:

key1:"value1" key2:"value2" key3:"value3"

在 reflect 包中定义了 StructTag 类型,表示 Tag 实例。

type StructTag string

从定义上可以看出,StructTag 是以字符串为基础的新类型。为了便于访问 Tag 中的条目,StructTag 类型提供以下两个方法:

  1. Get:根据 key 获取对应的 value。
  2. Lookup:根据 key 查找 value。如果找到,返回值 ok 为 true,并将结果存到 value 中;如果找不到,则 ok 为 false。

下面的示例演示了如何通过字段的 Tag 来为用户名和密码设置限制条件。当程序对用户信息进行检查时,会从 Tag 中读出相应的限制条件,如果所设置的用户名和密码与限制条件不符,则会输出错误信息。

步骤 1: 定义 User 结构体

type User struct {
    Username string `maxlen:"10"`
    Password string `minlen:"8" mask:"* "`
}

在 Username 字段的 Tag 中,maxlen 表示用户名的最大长度为 10 个字符,同样,在 Password 字段的 Tag 中,minlen 表示密码的最小长度为 8 个字符。mask 指定密码的掩码,掩码用于在屏幕输出密码时隐藏真实的密码内容,输出形如 “*****” 的文本。

步骤 2: 定义 validateUser 函数,用于检查用户名和密码是否满足限制条件

func validateUser(u User) {
    var typofUser = reflect.TypeOf(u)
    var valofUser = reflect.ValueOf(u)
    // 读出 Tag
    tagUn := typofUser.Field(0).Tag
    tagPwd := typofUser.Field(1).Tag
    // 解析用户名最大长度
    maxLenofUn, _ := strconv.ParseInt(tagUn.Get("maxlen"), 10, 0)
    // 解析密码最小长度
    minLenofPwd, _ := strconv.ParseInt(tagPwd.Get("minlen"), 10, 0)
    // 读取两个字段的值
    var usrName = valofUser.Field(0).String()
    var pssWd = valofUser.Field(1).String()
    // 开始验证
    unLen := utf8.RuneCountInString(usrName)
    pwdLen := utf8.RuneCountInString(pssWd)
    if unLen > int(maxLenofUn) {
        fmt.Printf("用户名 %s 长度超过 %d\n", usrName, maxLenofUn)
        return
    }
    if pwdLen < int(minLenofPwd) {
        // 获取密码掩码
        maskChar := tagPwd.Get("mask")
        // 生成类似"* * * * * * * *"的密码字符串
        displayPwd := strings.Repeat(maskChar, pwdLen)
        fmt.Printf("密码 %s 的长度不符,至少需要 %d 个字符\n", displayPwd, minLenofPwd)
        return
    }
    fmt.Println("用户信息符合要求")
}

步骤 3: 创建第一个 User 实例,并为字段赋值,然后调用 validateUser 函数进行检查

var u1 User
u1.Username = "jk_056546595xlase"
u1.Password = "65656565656"
validateUser(u1)

u1 对象设置的用户名过长,检查结果为:

用户名 jk_056546595xlase 长度超过10

步骤 4: 创建第二个 User 实例,并为字段赋值,然后进行检查

var u2 User
u2.Username = "Jack"
u2.Password = "321"
validateUser(u2)

u2 对象的密码长度不符合要求,检查结果为:

密码 *** 的长度不符,至少需要8个字符

步骤 5: 创建第三个 User 实例,并为字段赋值,然后进行检查

var u3 User
u3.Username = "Alice"
u3.Password = "UUI9ocg210359m82"
validateUser(u3)

u3 对象完全满足限制条件的要求,所以顺利通过检查。

补充:

在Go语言里,结构体标签(Tag)是依附于结构体字段的元数据字符串,它能为字段增添额外信息,这些信息可在运行时借助反射机制读取,从而实现序列化、数据验证、数据库映射等多种功能。

代码示例

下面的示例代码会展示结构体标签在JSON序列化、数据验证以及自定义业务逻辑中的应用。

package main

import (
	"encoding/json"
	"fmt"
	"reflect"
	"strconv"
	"strings"
)

// User 定义用户结构体,包含多个带标签的字段
type User struct {
	// JSON序列化标签,指定JSON键名及忽略空值
	Name     string `json:"user_name,omitempty" validate:"min=3,max=20" biz:"name"`
	Age      int    `json:"user_age" validate:"min=18,max=100" biz:"age"`
	Email    string `json:"user_email" validate:"email" biz:"email"`
	Password string `json:"-" validate:"min=6" biz:"password"`
}

// validate 自定义验证函数,根据标签规则验证结构体字段
func validate(u interface{}) error {
	v := reflect.ValueOf(u)
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	t := v.Type()
	for i := 0; i < v.NumField(); i++ {
		field := t.Field(i)
		tag := field.Tag.Get("validate")
		if tag == "" {
			continue
		}
		rules := strings.Split(tag, ",")
		value := v.Field(i)
		switch value.Kind() {
		case reflect.String:
			strValue := value.String()
			for _, rule := range rules {
				parts := strings.Split(rule, "=")
				if len(parts) == 2 {
					key, val := parts[0], parts[1]
					switch key {
					case "min":
						min, _ := strconv.Atoi(val)
						if len(strValue) < min {
							return fmt.Errorf("%s 长度不能小于 %d", field.Name, min)
						}
					case "max":
						max, _ := strconv.Atoi(val)
						if len(strValue) > max {
							return fmt.Errorf("%s 长度不能大于 %d", field.Name, max)
						}
					case "email":
						if !strings.Contains(strValue, "@") {
							return fmt.Errorf("%s 不是有效的邮箱地址", field.Name)
						}
					}
				}
			}
		case reflect.Int:
			intValue := value.Int()
			for _, rule := range rules {
				parts := strings.Split(rule, "=")
				if len(parts) == 2 {
					key, val := parts[0], parts[1]
					switch key {
					case "min":
						min, _ := strconv.Atoi(val)
						if intValue < int64(min) {
							return fmt.Errorf("%s 不能小于 %d", field.Name, min)
						}
					case "max":
						max, _ := strconv.Atoi(val)
						if intValue > int64(max) {
							return fmt.Errorf("%s 不能大于 %d", field.Name, max)
						}
					}
				}
			}
		}
	}
	return nil
}

func main() {
	// 创建用户实例
	user := User{
		Name:     "Alice",
		Age:      25,
		Email:    "alice@example.com",
		Password: "abcdef",
	}

	// 验证用户信息
	if err := validate(user); err != nil {
		fmt.Println("验证失败:", err)
		return
	}

	// JSON序列化
	data, err := json.Marshal(user)
	if err != nil {
		fmt.Println("JSON序列化失败:", err)
		return
	}
	fmt.Println("JSON序列化结果:", string(data))

	// 读取自定义业务标签
	t := reflect.TypeOf(user)
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		bizTag := field.Tag.Get("biz")
		fmt.Printf("%s 的业务标签是: %s\n", field.Name, bizTag)
	}
}

运行结果如下:

JSON序列化结果: {"user_name":"Alice","user_age":25,"user_email":"alice@example.com"}
Name 的业务标签是: name
Age 的业务标签是: age
Email 的业务标签是: email
Password 的业务标签是: password

代码解释

  1. 结构体定义User结构体中的每个字段都有多个标签。json标签用于指定JSON序列化时的键名以及是否忽略空值;validate标签用于定义字段的验证规则;biz标签是自定义的业务标签。
  2. 验证函数validate函数借助反射读取结构体字段的validate标签,依据规则对字段值进行验证,若不满足规则则返回错误。
  3. JSON序列化:在main函数里,先创建User实例,调用validate函数验证用户信息,验证通过后使用json.Marshal对用户信息进行JSON序列化并输出结果。
  4. 自定义业务标签读取:通过反射读取biz标签,输出每个字段的业务标签信息。

json标签:
json:“user_name,omitempty”:意味着在进行 JSON 序列化时,Name字段会以user_name作为键名;omitempty选项表示当Name字段为空字符串时,该字段不会出现在序列化后的 JSON 数据中。
json:“-”:表示Password字段在 JSON 序列化时会被忽略。
validate标签:
validate:“min=3,max=20”:针对Name字段定义了验证规则,要求其长度在 3 到 20 个字符之间。
validate:“min=18,max=100”:为Age字段设定了验证规则,要求其值在 18 到 100 之间。
validate:“email”:要求Email字段的值必须是一个有效的邮箱地址。
validate:“min=6”:规定Password字段的长度至少为 6 个字符。
biz标签:这是自定义的业务标签,为每个字段添加了业务相关的标识,像biz:“name” 、biz:"age"等,方便在业务逻辑里进行区分和处理。

通过这个示例,你能看到结构体标签在不同场景下的强大作用,可在实际开发中依据需求灵活运用。