结构体标签解析
结构体标签解析
一、什么是结构体标签
Go 的结构体字段可以附加一段字符串形式的元数据,称为标签(Tag)。标签写在字段类型后面,用反引号包裹:
type User struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age,omitempty"`
Secret string `json:"-"`
}
标签本身只是字符串,编译器不会对它做任何处理。它的意义完全取决于使用它的库——encoding/json 读 json 标签来决定 JSON 字段名和序列化规则,validator 库读 validate 标签来校验字段值,ORM 库读 db 或 gorm 标签来映射数据库列。
核心机制:标签的解析发生在运行时,通过反射读取。这正是 reflect 包与实际工程最常见的交汇点。
二、reflect.StructTag —— 标签解析器
2.1 StructField.Tag
反射遍历结构体字段时,每个 StructField 都有一个 Tag 字段,类型为 reflect.StructTag(底层是 string):
type User struct {
Name string `json:"name" db:"user_name"`
}
t := reflect.TypeOf(User{})
f, _ := t.FieldByName("Name")
fmt.Println(f.Tag) // json:"name" db:"user_name"
fmt.Println(f.Tag.Get("json")) // name
fmt.Println(f.Tag.Get("db")) // user_name
2.2 Get 与 Lookup
StructTag 提供两个方法:
| 方法 | 返回 | 说明 |
|---|---|---|
Get(key string) string |
标签值字符串 | 找不到返回空串 "" |
Lookup(key string) (string, bool) |
值 + 是否存在 | 区分"标签值为空"和"标签不存在" |
type Item struct {
ID int `json:"id"`
Notes string `json:""` // json 标签值为空
Extra string // 没有任何标签
}
t := reflect.TypeOf(Item{})
// Get 无法区分空值和不存在
f0, _ := t.FieldByName("ID")
f1, _ := t.FieldByName("Notes")
f2, _ := t.FieldByName("Extra")
fmt.Println(f0.Tag.Get("json")) // "id"
fmt.Println(f1.Tag.Get("json")) // "" (空值)
fmt.Println(f2.Tag.Get("json")) // "" (不存在)
// Lookup 可以区分
v, ok := f1.Tag.Lookup("json") // ("", true) —— 标签存在但值为空
v, ok = f2.Tag.Lookup("json") // ("", false) —— 标签不存在
这是一个关键的区别:当 JSON 标签显式写 json:"" 时,encoding/json 会使用字段原名;当没有 json 标签时,默认行为取决于版本。Lookup 让你能精确区分这两种情况。
2.3 标签语法规则
标签遵循一个约定的格式,StructTag 的解析器按此格式工作:
key1:"value1" key2:"value2" key3:"value3,option1,option2"
规则:
- 键值用冒号分隔,值用双引号包裹
- 键值之间用空格分隔(不是逗号!逗号只在值内部使用)
- 值内部可以包含逗号分隔的选项,如
json:"name,omitempty" - 值中的逗号选项不会被 Tag 解析器拆分——
Get("json")返回完整的"name,omitempty",需要你自己拆分选项
type Config struct {
Port int `json:"port,omitempty" env:"SERVER_PORT" default:"8080"`
}
t := reflect.TypeOf(Config{})
f, _ := t.FieldByName("Port")
fmt.Println(f.Tag.Get("json")) // "port,omitempty"
fmt.Println(f.Tag.Get("env")) // "SERVER_PORT"
fmt.Println(f.Tag.Get("default")) // "8080"
2.4 常见标签约定
| 键 | 用途 | 示例 |
|---|---|---|
json |
JSON 序列化/反序列化 | json:"name,omitempty" |
xml |
XML 编码 | xml:"fullname,attr" |
db |
数据库列映射 | db:"user_name" |
gorm |
GORM ORM 配置 | gorm:"column:user_name;type:varchar(100)" |
validate |
字段校验规则 | validate:"required,email" |
env |
环境变量名 | env:"SERVER_PORT" |
yaml |
YAML 序列化 | yaml:"server_port" |
三、json 标签的常用选项
encoding/json 是标签使用最广泛的场景,它的值内部支持逗号分隔的选项:
| 选项 | 含义 |
|---|---|
omitempty |
字段为零值时跳过序列化 |
- |
忽略该字段(不序列化也不反序列化) |
string |
数值类型序列化为字符串(JSON中的数字是字符串形式) |
type Response struct {
Code int `json:"code"`
Message string `json:"msg,omitempty"` // 空串时不输出
Count int64 `json:"count,string"` // 输出为字符串 "100" 而非 100
Internal string `json:"-"` // 完全忽略
}
r := Response{Code: 200, Count: 100, Internal: "secret"}
data, _ := json.Marshal(r)
fmt.Println(string(data))
// {"code":200,"count":"100"} — Message 为空被省略,Internal 被忽略
四、实战:自定义标签解析器
理解了标签机制后,我们可以实现一个简单的结构化数据校验器,这是标签在实际工程中的典型应用:
package main
import (
"fmt"
"reflect"
"strconv"
"strings"
)
type Rule struct {
Required bool
Min int
Max int
}
func parseValidateTag(tag string) Rule {
rule := Rule{}
parts := strings.Split(tag, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "required" {
rule.Required = true
} else if strings.HasPrefix(p, "min=") {
rule.Min, _ = strconv.Atoi(strings.TrimPrefix(p, "min="))
} else if strings.HasPrefix(p, "max=") {
rule.Max, _ = strconv.Atoi(strings.TrimPrefix(p, "max="))
}
}
return rule
}
func Validate(s interface{}) []string {
var errors []string
v := reflect.ValueOf(s)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fv := v.Field(i)
// 解析 validate 标签
tagStr, ok := field.Tag.Lookup("validate")
if !ok {
continue
}
rule := parseValidateTag(tagStr)
// 必填检查
if rule.Required && fv.IsZero() {
errors = append(errors, fmt.Sprintf("%s: 不能为空", field.Name))
continue
}
// 范围检查(仅对整数类型)
if fv.Kind() == reflect.Int && (rule.Min > 0 || rule.Max > 0) {
val := fv.Int()
if val < int64(rule.Min) {
errors = append(errors, fmt.Sprintf("%s: 值 %d 小于最小值 %d", field.Name, val, rule.Min))
}
if rule.Max > 0 && val > int64(rule.Max) {
errors = append(errors, fmt.Sprintf("%s: 值 %d 大于最大值 %d", field.Name, val, rule.Max))
}
}
}
return errors
}
五、练习代码
// struct_tag_practice.go
package main
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
)
// Registration 表单结构体,展示各种标签用法
type Registration struct {
Username string `json:"username" validate:"required" db:"user_name"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age,omitempty" validate:"min=18,max=120" db:"user_age"`
Phone string `json:"phone,omitempty"`
Internal string `json:"-"`
}
func main() {
// 练习1: 标签读取与 Lookup
fmt.Println("=== 练习1: 标签读取与 Lookup ===")
tagReadDemo()
// 练习2: json 标签序列化效果
fmt.Println("\n=== 练习2: json 标签序列化效果 ===")
jsonTagDemo()
// 练习3: 自定义校验器
fmt.Println("\n=== 练习3: 自定义校验器 ===")
validateDemo()
// 练习4: 标签信息汇总打印
fmt.Println("\n=== 练习4: 标签信息汇总打印 ===")
tagSummaryDemo()
}
func tagReadDemo() {
t := reflect.TypeOf(Registration{})
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
// Get vs Lookup 对比
jsonVal := f.Tag.Get("json")
jsonLookup, jsonOk := f.Tag.Lookup("json")
valLookup, valOk := f.Tag.Lookup("validate")
fmt.Printf(" %s: json.Get=%q, json.Lookup=(%q,%t), validate.Lookup=(%q,%t)\n",
f.Name, jsonVal, jsonLookup, jsonOk, valLookup, valOk)
}
}
func jsonTagDemo() {
// 完整数据
r1 := Registration{
Username: "alice",
Email: "alice@example.com",
Age: 25,
Phone: "13800001111",
Internal: "secret",
}
data1, _ := json.Marshal(r1)
fmt.Printf(" 完整数据: %s\n", string(data1))
// 部分字段为零值(观察 omitempty 和 - 的效果)
r2 := Registration{
Username: "bob",
Email: "bob@test.com",
Age: 0, // 零值,omitempty 会跳过
Phone: "", // 零值,omitempty 会跳过
Internal: "hidden",
}
data2, _ := json.Marshal(r2)
fmt.Printf(" 部分零值: %s\n", string(data2))
}
func validateDemo() {
// 正确数据
good := Registration{Username: "alice", Email: "a@b.com", Age: 25}
errs := ValidateStruct(good)
fmt.Printf(" 合法数据: errors=%v\n", errs)
// 错误数据
bad := Registration{Username: "", Email: "", Age: 10}
errs = ValidateStruct(bad)
fmt.Printf(" 非法数据: errors=%v\n", errs)
}
func ValidateStruct(s interface{}) []string {
var errors []string
v := reflect.ValueOf(s)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fv := v.Field(i)
tagStr, ok := f.Tag.Lookup("validate")
if !ok {
continue
}
parts := strings.Split(tagStr, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "required" && fv.IsZero() {
errors = append(errors, fmt.Sprintf("%s 不能为空", f.Name))
} else if strings.HasPrefix(p, "min=") {
minVal, _ := strconv.Atoi(strings.TrimPrefix(p, "min="))
if fv.Kind() == reflect.Int && fv.Int() < int64(minVal) {
errors = append(errors, fmt.Sprintf("%s 值%d < 最小值%d", f.Name, fv.Int(), minVal))
}
} else if strings.HasPrefix(p, "max=") {
maxVal, _ := strconv.Atoi(strings.TrimPrefix(p, "max="))
if fv.Kind() == reflect.Int && fv.Int() > int64(maxVal) {
errors = append(errors, fmt.Sprintf("%s 值%d > 最大值%d", f.Name, fv.Int(), maxVal))
}
}
}
}
return errors
}
func tagSummaryDemo() {
t := reflect.TypeOf(Registration{})
fmt.Printf(" 结构体: %s\n", t.Name())
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Printf(" %-10s %-8s 标签: %s\n", f.Name, f.Type, f.Tag)
// 解析每个标签键
for _, key := range []string{"json", "validate", "db"} {
val, ok := f.Tag.Lookup(key)
if ok {
fmt.Printf(" %-10s => %s\n", key, val)
}
}
}
}
运行结果
=== 练习1: 标签读取与 Lookup ===
Username: json.Get="username", json.Lookup=("username",true), validate.Lookup=("required",true)
Email: json.Get="email", json.Lookup=("email",true), validate.Lookup=("required,email",true)
Age: json.Get="age,omitempty", json.Lookup=("age,omitempty",true), validate.Lookup=("min=18,max=120",true)
Phone: json.Get="phone,omitempty", json.Lookup=("phone,omitempty",true), validate.Lookup=("",false)
Internal: json.Get="-", json.Lookup=("-",true), validate.Lookup=("",false)
=== 练习2: json 标签序列化效果 ===
完整数据: {"username":"alice","email":"alice@example.com","age":25,"phone":"13800001111"}
部分零值: {"username":"bob","email":"bob@test.com"}
=== 练习3: 自定义校验器 ===
合法数据: errors=[]
非法数据: errors=[Username 不能为空, Email 不能为空, Age 值10 < 最小值18]
=== 练习4: 标签信息汇总打印 ===
结构体: Registration
Username string 标签: json:"username" validate:"required" db:"user_name"
json => username
validate => required
db => user_name
Email string 标签: json:"email" validate:"required,email"
json => email
validate => required,email
Age int 标签: json:"age,omitempty" validate:"min=18,max=120" db:"user_age"
json => age,omitempty
validate => min=18,max=120
db => user_age
Phone string 标签: json:"phone,omitempty"
json => phone,omitempty
Internal string 标签: json:"-"
json => -
六、知识点小结
| 概念 | 要点 |
|---|---|
| 结构体标签 | 反引号包裹的字符串元数据,写在字段类型后 |
| StructTag | 底层是 string,有 Get/Lookup 两个方法 |
| Get vs Lookup | Get 找不到返回空串;Lookup 返回 (值, 是否存在),能区分空值和不存在 |
| 标签格式 | key:"value" key2:"value2",键值用冒号,对间用空格 |
| 值内选项 | json:"name,omitempty" 中逗号分隔选项,需自己拆分 |
| json 选项 | omitempty(零值跳过)、-(忽略)、string(数字转字符串) |
| 实际应用 | JSON序列化、ORM映射、数据校验、配置管理等 |
| 自定义解析 | strings.Split 拆分选项,strconv 转换数值,反射读取字段值 |

浙公网安备 33010602011771号