Go 高手(04):reflect 反射——Type、Value、调用、性能开销
更新时间:2026-09-01。本文是
languages/go/intermediate/高手层第 4 篇,接 io 包与接口设计。Go 是静态语言,但reflect包让你在运行时能获取类型信息、读写结构体字段、动态调用方法。这是 JSON、ORM 这些库的核心技术,但反射也有性能和类型安全的代价。
本文要回答的问题
reflect.TypeOf和reflect.ValueOf有什么区别?- 怎么用反射读取结构体字段?怎么获取 struct tag?
- 怎么动态调用一个方法?
- 反射性能比直接代码差多少?为什么要尽量少用?
encoding/json里 struct tag 是怎么解析的?
一、反射基础:TypeOf 和 ValueOf
go
import "reflect"
var x int = 42
t := reflect.TypeOf(x) // 类型信息:int
v := reflect.ValueOf(x) // 值信息:42
fmt.Println(t.Kind()) // int
fmt.Println(t.Name()) // "int"
fmt.Println(v.Int()) // 42- Type:类型信息,包括
Kind(int/struct/pointer/func 等)、字段、方法等 - Value:值信息,可以用来读、写实际的值
二、Kind vs Type
很多人搞混 Kind 和 Type:
go
type MyInt int
var v MyInt = 42
t := reflect.TypeOf(v)
fmt.Println(t.Name()) // "MyInt" (类型名)
fmt.Println(t.Kind()) // int (底层类别)Type是完整的 Go 类型,包括用户定义类型Kind是底层的类别(基本类别:int、struct、pointer、slice、map、func、interface...)
所以,对于用户定义类型,Kind() 告诉你"底层是什么类别",Name() 告诉你"叫什么名字"。
三、结构体反射:读取字段和 tag
go
type User struct {
Name string `json:"name" validate:"required"`
Age int `json:"age"`
}
u := User{Name: "Alice", Age: 25}
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
// 遍历所有字段
for i := 0; i < t.NumField(); i++ {
field := t.Field(i) // 字段信息
value := v.Field(i) // 字段值
fmt.Printf("name: %s, type: %v\n", field.Name, field.Type)
fmt.Printf("json tag: %s\n", field.Tag.Get("json"))
}输出:
name: Name, type: string
json tag: name
name: Age, type: int
json tag: agefield.Tag 就是 struct tag,可以用 Get("key") 取出对应的值——这就是 encoding/json 解析 json:"name" tag 的原理。
四、反射修改值:为什么要用 Elem()?
要修改值,必须拿到指针:
go
x := 42
v := reflect.ValueOf(x)
v.SetInt(100) // ❌ panic:x 是值拷贝,不可寻址
// ✅ 正确:传指针,然后 Elem() 解引用
x := 42
v := reflect.ValueOf(&x).Elem()
v.SetInt(100)
fmt.Println(x) // 100规则:
reflect.ValueOf传指针才能拿到可寻址的值- 用
Elem()解引用指针,得到底层值的 Value - 只有可寻址的 Value 才能用
Set*修改
五、动态调用方法
go
type Calculator struct{}
func (c *Calculator) Add(a, b int) int {
return a + b
}
// 动态调用
calc := &Calculator{}
v := reflect.ValueOf(calc)
method := v.MethodByName("Add")
args := []reflect.Value{reflect.ValueOf(2), reflect.ValueOf(3)}
result := method.Call(args)
fmt.Println(result[0].Int()) // 5反射可以按名字找方法,按参数列表动态调用——这是 RPC 框架、ORM 的核心技术。
六、反射性能:为什么尽量少用?
性能对比(百万次调用):
| 方式 | 耗时 |
|---|---|
| 直接调用 | ~几毫秒 |
| 反射调用 | ~几十到几百毫秒 |
反射比直接调用慢 5~10 倍甚至更多,原因:
- 反射需要动态类型检查,编译器没法做优化
- 参数传递要用
[]reflect.Value,有分配开销 - 结构体字段查找是 O(n) 的
什么时候用反射?
- 写框架(json、ORM、RPC)时,不得不应对任意类型
- 应用层代码尽量不要用反射
- 能用接口做的,就不用反射
七、实战:用反射解析 struct tag,做简单验证
go
func Validate(s any) error {
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
required := field.Tag.Get("validate") == "required"
if required && value.IsZero() {
return fmt.Errorf("field %s is required", field.Name)
}
}
return nil
}
// 使用
type User struct {
Name string `validate:"required"`
Age int
}
user := User{}
err := Validate(user) // field Name is required这就是很多 Go 验证库的基础原理。
八、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 修改值忘记 Elem() | panic: reflect: reflect.Value.SetInt using unaddressable value | 传指针,然后 .Elem() |
| 结构体私有字段(小写开头)反射拿不到 | 返回零值 | 反射只能读/写导出字段(大写开头) |
| 反射创建 slice | 创建出来是空 slice,直接索引越界 | 用 reflect.MakeSlice 创建 |
| 太多反射导致性能差 | 整个程序变慢 | 框架层用,应用层少用 |
相关与延伸
下一篇:defer——执行顺序、捕获返回值、性能陷阱;结构体 tag 就是给框架留的元信息,JSON 序列化就是用反射解析,见 JSON 序列化。
一句话总结
Go reflect 反射:reflect.TypeOf 拿类型信息,reflect.ValueOf 拿值信息;Kind 是底层类别,Type 是完整类型;结构体字段可以读 tag、拿值;要修改必须传指针并用 Elem() 解引用;动态调用方法用 Value.MethodByName() 然后 Call();反射比直接调用慢很多,尽量少用,框架层才需要。