Go 反射入门:概念、应用场景与 JSON 序列化原理
我们聊一个问题:
为什么 json.Marshal 可以接收任意结构体?比如:
json.Marshal(User{}) json.Marshal(Product{}) json.Marshal([]Order{})json.Marshal在编译时并不知道你会传什么类型。
但是它运行时却能知道:
- 你传进来的是不是结构体
- 结构体有几个字段
- 字段叫什么
- 字段值是多少
- 字段上有没有
jsontag - 哪些字段要忽略
- 哪些字段空值不输出
这背后靠的就是 Go 的反射。
一句话先说结论:
反射就是程序在运行时查看和操作类型信息、字段信息、方法信息和值。它让程序在“不提前知道具体类型”的情况下,仍然可以写出通用逻辑。
一、什么是反射
平时写 Go 代码,大多数类型信息在编译期就确定了。
例如:
type User struct { Name string Age int } func PrintUser(u User) { fmt.Println(u.Name) fmt.Println(u.Age) }这里编译器很清楚:
u 是 User u 有 Name 字段 u 有 Age 字段 Name 是 string Age 是 int所以你可以直接写:
u.Name u.Age但是如果函数参数是:
func Print(v any) {}问题就来了:
v 可能是 User v 可能是 Product v 可能是 []int v 可能是 map[string]string v 也可能是 nil编译期不知道具体类型,普通代码就没法直接写:
v.Name因为any不保证有Name字段。
这时就可以用反射,在运行时问它:
你到底是什么类型? 你是什么种类? 你有哪些字段? 字段值是多少? 字段上有没有 tag? 能不能修改?二、Go 反射的两个入口:TypeOf 和 ValueOf
Go 的反射主要在reflect包里。
最常用的两个函数是:
reflect.TypeOf(v) reflect.ValueOf(v)可以这样理解:
TypeOf 看类型信息 ValueOf 看值信息完整例子:
package main import ( "fmt" "reflect" ) func main() { var x any = 123 t := reflect.TypeOf(x) v := reflect.ValueOf(x) fmt.Println("type:", t) fmt.Println("kind:", t.Kind()) fmt.Println("value:", v) fmt.Println("value kind:", v.Kind()) }输出:
type: int kind: int value: 123 value kind: int这里的x是一个接口值,里面实际装的是int。
reflect.TypeOf(x)拿到的是动态类型:
intreflect.ValueOf(x)拿到的是运行时值:
123三、Type 和 Kind 有什么区别
反射里经常看到两个词:
Type Kind它们很像,但不是一回事。
Type是具体类型。
Kind是底层分类。
看例子:
package main import ( "fmt" "reflect" ) type UserID int func main() { var id UserID = 100 t := reflect.TypeOf(id) fmt.Println("type:", t) fmt.Println("name:", t.Name()) fmt.Println("kind:", t.Kind()) }输出:
type: main.UserID name: UserID kind: int解释一下:
Type 是 main.UserID Kind 是 int因为UserID是你定义的新类型,但它的底层种类是int。
再看结构体:
type User struct { Name string }它的:
Type 是 main.User Kind 是 struct新手可以这样记:
Type 更具体:你到底叫什么类型 Kind 更粗略:你属于哪一类常见 Kind 包括:
reflect.Bool reflect.Int reflect.String reflect.Struct reflect.Slice reflect.Map reflect.Ptr reflect.Interface四、遍历结构体字段
反射最常见的用途之一,就是遍历结构体字段。
普通代码里,如果你知道类型,可以直接访问字段:
user.Name user.Age但如果你写的是通用函数,不知道传进来是什么结构体,就要用反射。
示例:
package main import ( "fmt" "reflect" ) type User struct { Name string Age int } func PrintFields(v any) { rv := reflect.ValueOf(v) rt := reflect.TypeOf(v) if rv.Kind() != reflect.Struct { fmt.Println("not a struct") return } for i := 0; i < rv.NumField(); i++ { fieldInfo := rt.Field(i) fieldValue := rv.Field(i) fmt.Printf("%s = %v\n", fieldInfo.Name, fieldValue) } } func main() { user := User{Name: "Tom", Age: 18} PrintFields(user) }输出:
Name = Tom Age = 18这里几个 API 很重要:
rv.NumField()返回结构体有几个字段。
rt.Field(i)返回第i个字段的类型信息,比如字段名、字段类型、tag。
rv.Field(i)返回第i个字段的值。
简单理解:
reflect.Type 负责字段说明书 reflect.Value 负责字段实际值五、struct tag 是什么
结构体 tag 是写在字段后面的元信息。
例如:
type User struct { Name string `json:"name"` Age int `json:"age"` }这里:
`json:"name"` `json:"age"`就是 tag。
tag 不会直接改变字段本身。
它更像写给框架或标准库看的说明:
这个字段转 JSON 时叫 name 这个字段转 JSON 时叫 age读取 tag 也要用反射。
示例:
package main import ( "fmt" "reflect" ) type User struct { Name string `json:"name" validate:"required"` Age int `json:"age"` } func main() { t := reflect.TypeOf(User{}) for i := 0; i < t.NumField(); i++ { field := t.Field(i) fmt.Println("field:", field.Name) fmt.Println("json tag:", field.Tag.Get("json")) fmt.Println("validate tag:", field.Tag.Get("validate")) fmt.Println("---") } }输出:
field: Name json tag: name validate tag: required --- field: Age json tag: age validate tag: ---常见 tag 有:
json:"name" yaml:"name" db:"name" gorm:"column:name" validate:"required" form:"name"不同库会读取不同 tag。
例如:
encoding/json读取json- ORM 可能读取
db、gorm - 参数校验库可能读取
validate - Web 框架可能读取
form
一句话:
tag 是结构体字段上的说明书,反射是读取说明书的工具。六、类型断言和反射有什么区别
你可能会问:
v.(T)不是也能知道类型吗?
是,但它和反射不是一回事。
类型断言适合这种场景:
我知道它可能是 string,帮我确认一下。例如:
package main import "fmt" func Print(v any) { if s, ok := v.(string); ok { fmt.Println("string:", s) return } if n, ok := v.(int); ok { fmt.Println("int:", n) return } fmt.Println("unknown") } func main() { Print("hello") Print(100) Print(true) }输出:
string: hello int: 100 unknown这里你提前写死了:
string int反射适合这种场景:
我不知道它是什么结构体,但我想遍历它的字段和 tag。对比一下:
| 能力 | 类型断言 | 反射 |
|---|---|---|
| 是否需要提前知道目标类型 | 需要 | 不需要 |
| 能否遍历未知结构体字段 | 不适合 | 可以 |
| 能否读取 struct tag | 不行 | 可以 |
| 性能 | 通常更好 | 通常更慢 |
| 可读性 | 更简单 | 更复杂 |
| 常见用途 | 判断少数已知类型 | JSON、ORM、校验器、框架 |
所以优先级一般是:
能用普通代码就用普通代码 能用接口就用接口 能用类型断言就用类型断言 确实需要通用运行时类型处理,再用反射七、反射的三条直觉规则
Go 官方博客《The Laws of Reflection》里讲过反射的几个基本规律。
新手可以先不用背原文,记住这三个直觉:
1. 从普通值到反射对象
v := reflect.ValueOf(x) t := reflect.TypeOf(x)你把普通值交给反射,得到反射对象。
2. 从反射对象回到普通值
x := v.Interface()Interface()可以把reflect.Value重新变成any。
然后你可以做类型断言:
s := x.(string)3. 想修改值,必须传指针并且值可设置
看例子:
package main import ( "fmt" "reflect" ) func main() { name := "Tom" v := reflect.ValueOf(&name).Elem() if v.CanSet() { v.SetString("Jerry") } fmt.Println(name) }输出:
Jerry为什么要传&name?
因为如果你只传:
reflect.ValueOf(name)反射拿到的是一份值,不知道原变量在哪里,不能修改原变量。
传指针后:
reflect.ValueOf(&name).Elem()反射才能找到原变量的位置。
一句话:
反射修改值时,要拿到可寻址、可设置的值。八、反射的应用场景
反射不是日常业务代码里到处用的东西。
它更常出现在框架和通用库里。
1. JSON / XML / YAML 序列化
例如:
json.Marshal(user)encoding/json要在运行时读取结构体字段、字段值和jsontag。
2. ORM 数据库映射
例如:
type User struct { ID int `db:"id"` Name string `db:"name"` }ORM 可以通过反射知道:
User.ID 对应数据库 id 字段 User.Name 对应数据库 name 字段3. 参数校验
例如:
type RegisterRequest struct { Email string `validate:"required,email"` Age int `validate:"gte=18"` }校验库会读取validatetag,检查字段是否合法。
4. 配置解析
例如把配置文件填充到结构体:
type Config struct { Port int `yaml:"port"` Mode string `yaml:"mode"` }配置库通过反射知道字段名、字段类型,再把文本值转换进去。
5. RPC / Web 框架参数绑定
Web 框架经常支持:
func CreateUser(req CreateUserRequest) {}框架要把 HTTP 请求里的 JSON、form、query 参数绑定到结构体字段,也需要读取类型和 tag。
6. 通用调试工具
比如打印任意结构体字段、比较两个值、深拷贝、对象转 map 等。
这些工具往往不知道具体类型,所以会使用反射。
九、反射的缺点
反射很强,但不要滥用。
主要缺点有:
1. 代码复杂
普通代码:
user.Name反射代码:
v.FieldByName("Name")后者更绕,也更容易写错。
2. 运行时才发现问题
普通代码字段写错,编译器会报错。
反射代码字段名写错,可能运行时才发现。
3. 性能更差
反射需要运行时检查类型和值,通常比直接访问字段慢。
对于普通业务逻辑,不要为了“高级”而使用反射。
4. 容易 panic
例如:
v.Field(100) v.SetString("x")如果字段不存在,或者值不可设置,就可能 panic。
所以反射代码经常要检查:
Kind() IsValid() CanSet() CanInterface() IsNil()十、为什么 JSON 序列化需要反射
现在回到核心问题:
json.Marshal(v any)它的参数是any。
这表示你可以传任意类型:
json.Marshal(User{}) json.Marshal(Product{}) json.Marshal([]int{1, 2, 3}) json.Marshal(map[string]string{"name": "Tom"})encoding/json在编译期不知道你会传什么。
所以它必须在运行时做类似这些事:
1. 判断传进来的是 struct、slice、map、string、int 还是 bool 2. 如果是 struct,就遍历字段 3. 读取字段值 4. 读取 json tag 5. 忽略 json:"-" 的字段 6. 处理 json:"name,omitempty" 7. 递归处理嵌套结构 8. 生成 JSON 字符串例如:
type User struct { Name string `json:"name"` Age int `json:"age"` Password string `json:"-"` }json.Marshal要知道:
Name 字段输出成 name Age 字段输出成 age Password 字段忽略这些都来自结构体字段和 tag。
普通代码不知道未知结构体有哪些字段,只能靠反射。
十一、标准库 json.Marshal 示例
先看标准库自己的行为:
package main import ( "encoding/json" "fmt" ) type User struct { Name string `json:"name"` Age int `json:"age"` Email string `json:"email,omitempty"` Password string `json:"-"` } func main() { user := User{ Name: "Tom", Age: 18, Email: "", Password: "secret", } data, err := json.Marshal(user) if err != nil { fmt.Println("marshal error:", err) return } fmt.Println(string(data)) }输出:
{"name":"Tom","age":18}为什么没有Email?
因为:
json:"email,omitempty"Email是空字符串,omitempty表示空值不输出。
为什么没有Password?
因为:
json:"-"表示忽略这个字段。
十二、手写一个迷你版 JSON 序列化器
下面我们手写一个简化版SimpleMarshal。
它支持:
- struct
- pointer
- string
- int
- bool
- slice / array
- map[string]T
json:"name"json:"-"omitempty
它不是为了替代标准库,而是为了理解反射在 JSON 序列化里做了什么。
完整代码:
package main import ( "fmt" "reflect" "sort" "strconv" "strings" ) type User struct { Name string `json:"name"` Age int `json:"age"` Email string `json:"email,omitempty"` Password string `json:"-"` Active bool `json:"active"` Tags []string `json:"tags,omitempty"` } func SimpleMarshal(v any) (string, error) { return marshalValue(reflect.ValueOf(v)) } func marshalValue(v reflect.Value) (string, error) { if !v.IsValid() { return "null", nil } if v.Kind() == reflect.Interface { if v.IsNil() { return "null", nil } return marshalValue(v.Elem()) } if v.Kind() == reflect.Ptr { if v.IsNil() { return "null", nil } return marshalValue(v.Elem()) } switch v.Kind() { case reflect.Struct: return marshalStruct(v) case reflect.String: return strconv.Quote(v.String()), nil case reflect.Bool: if v.Bool() { return "true", nil } return "false", nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(v.Int(), 10), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return strconv.FormatUint(v.Uint(), 10), nil case reflect.Slice, reflect.Array: return marshalSlice(v) case reflect.Map: return marshalMap(v) default: return "", fmt.Errorf("unsupported kind: %s", v.Kind()) } } func marshalStruct(v reflect.Value) (string, error) { t := v.Type() parts := make([]string, 0, t.NumField()) for i := 0; i < t.NumField(); i++ { field := t.Field(i) value := v.Field(i) // PkgPath 不为空表示非导出字段。 // 非导出字段不能安全地通过 Interface 读取,JSON 也不会导出它。 if field.PkgPath != "" { continue } tag := field.Tag.Get("json") if tag == "-" { continue } name, omitempty := parseJSONTag(tag) if name == "" { name = field.Name } if omitempty && value.IsZero() { continue } encodedValue, err := marshalValue(value) if err != nil { return "", fmt.Errorf("marshal field %s: %w", field.Name, err) } parts = append(parts, strconv.Quote(name)+":"+encodedValue) } return "{" + strings.Join(parts, ",") + "}", nil } func marshalSlice(v reflect.Value) (string, error) { parts := make([]string, 0, v.Len()) for i := 0; i < v.Len(); i++ { encodedValue, err := marshalValue(v.Index(i)) if err != nil { return "", err } parts = append(parts, encodedValue) } return "[" + strings.Join(parts, ",") + "]", nil } func marshalMap(v reflect.Value) (string, error) { if v.Type().Key().Kind() != reflect.String { return "", fmt.Errorf("only map with string keys is supported") } keys := make([]string, 0, v.Len()) for _, key := range v.MapKeys() { keys = append(keys, key.String()) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, key := range keys { value := v.MapIndex(reflect.ValueOf(key)) encodedValue, err := marshalValue(value) if err != nil { return "", err } parts = append(parts, strconv.Quote(key)+":"+encodedValue) } return "{" + strings.Join(parts, ",") + "}", nil } func parseJSONTag(tag string) (name string, omitempty bool) { if tag == "" { return "", false } items := strings.Split(tag, ",") name = items[0] for _, item := range items[1:] { if item == "omitempty" { omitempty = true } } return name, omitempty } func main() { user := User{ Name: "Tom", Age: 18, Email: "", Password: "secret", Active: true, Tags: []string{"go", "reflect"}, } text, err := SimpleMarshal(user) if err != nil { fmt.Println("marshal error:", err) return } fmt.Println(text) more, err := SimpleMarshal(map[string]any{ "name": "Jerry", "age": 20, }) if err != nil { fmt.Println("marshal map error:", err) return } fmt.Println(more) }输出:
{"name":"Tom","age":18,"active":true,"tags":["go","reflect"]} {"age":20,"name":"Jerry"}十三、逐段拆解这个序列化器
入口函数很短:
func SimpleMarshal(v any) (string, error) { return marshalValue(reflect.ValueOf(v)) }它把任意值转成reflect.Value,交给marshalValue。
marshalValue做的事情是按种类分发:
switch v.Kind() { case reflect.Struct: return marshalStruct(v) case reflect.String: return strconv.Quote(v.String()), nil case reflect.Bool: // ... case reflect.Slice, reflect.Array: return marshalSlice(v) case reflect.Map: return marshalMap(v) }这就是 JSON 序列化的核心思路:
先判断值的种类,再按不同种类编码。处理指针
if v.Kind() == reflect.Ptr { if v.IsNil() { return "null", nil } return marshalValue(v.Elem()) }如果传进来的是:
&User{Name: "Tom"}Kind()会是:
ptr要用:
v.Elem()拿到指针指向的结构体。
处理结构体
结构体序列化的关键是:
t := v.Type() for i := 0; i < t.NumField(); i++ { field := t.Field(i) value := v.Field(i) }field是字段元信息:
字段名 字段类型 字段 tag 是否导出value是字段值:
Tom 18 true读取 tag:
tag := field.Tag.Get("json")忽略字段:
if tag == "-" { continue }解析omitempty:
name, omitempty := parseJSONTag(tag) if omitempty && value.IsZero() { continue }字段名处理:
if name == "" { name = field.Name }最后拼成:
"name":"Tom"为什么跳过非导出字段
Go 里小写字段是非导出的:
type User struct { Name string age int }标准库 JSON 不会导出小写字段。
反射里可以用:
field.PkgPath != ""判断字段是否非导出。
所以上面的代码写了:
if field.PkgPath != "" { continue }十四、真实 encoding/json 比这个复杂得多
我们写的SimpleMarshal只是教学版。
真实的encoding/json要处理更多情况:
- float
- nil slice
- nil map
- 嵌套结构体
- 匿名字段
- 字段冲突
- HTML 转义
json.Marshalerencoding.TextMarshaler- map key 排序
- 循环引用检测
- 更完整的 tag 规则
- 错误类型和边界情况
所以不要在生产环境使用这个教学版。
它的价值是帮助你理解:
JSON 序列化器为什么能处理任意结构体。原因就是:
反射可以在运行时检查值的类型、字段、tag 和字段值。十五、什么时候不要用反射
如果你明确知道类型,直接写普通代码。
不需要反射:
func PrintUser(user User) { fmt.Println(user.Name) }不需要写成:
func PrintUser(user User) { v := reflect.ValueOf(user) fmt.Println(v.FieldByName("Name")) }如果你只是想支持多态,优先用接口:
type Writer interface { Write([]byte) (int, error) }如果你只是关心少数几个类型,优先用类型断言或 type switch。
反射适合:
你写的是通用框架或库 你不知道调用方会传什么结构体 你需要读取字段和 tag 你需要按类型动态处理值十六、新手学习路线
你可以按这个顺序练习反射:
- 用
reflect.TypeOf打印变量类型。 - 用
reflect.ValueOf打印变量值。 - 区分
Type和Kind。 - 遍历结构体字段。
- 读取 struct tag。
- 处理指针:
Kind() == reflect.Ptr和Elem()。 - 用
CanSet修改一个变量。 - 写一个
StructToMap小工具。 - 写一个迷你版 JSON 序列化器。
- 再去看
encoding/json的真实行为。
反射不需要一开始就学得很深。
你先理解:
Type 看类型 Value 看值 Kind 看种类 StructField 看字段说明 Tag 看字段标签就能读懂很多框架代码了。
总结
Go 反射可以概括成几句话:
- 反射让程序在运行时检查类型和值。
reflect.TypeOf获取类型信息。reflect.ValueOf获取值信息。Type是具体类型,Kind是底层分类。- 结构体字段和 struct tag 可以通过反射读取。
- JSON、ORM、参数校验、配置解析、Web 框架经常使用反射。
- 类型断言适合已知类型,反射适合未知结构的通用处理。
- 反射更灵活,但也更复杂、更慢、更容易 panic。
encoding/json能处理任意结构体,本质上就是用反射读取字段、字段值和jsontag。
最后记住一句:
反射不是日常业务代码的优先选择,而是编写通用框架和通用工具时的能力。理解反射之后,你再看json.Marshal、ORM、validate、配置解析,就不会觉得它们像魔法了。
参考资料
- Package reflect
- The Laws of Reflection
- Package encoding/json
- Go Wiki: Well-known struct tags
- Package encoding