Go反射reflect包的用法和注意点
反射是 Go 在运行时审视和操作自身类型与值的能力,入口是 reflect.TypeOf 取类型、reflect.ValueOf 取值。读结构体用 NumField 加 Field 遍历,改值必须传指针用 Elem 再 CanSet 否则 panic,还能用 MethodByName 按名字调方法。但反射慢、丢编译检查,是框架利器不是业务日常。
Go 是门静态类型语言,类型在编译期就定死了。但有些场景,我们需要在运行时才知道一个变量到底是什么类型、有哪些字段、能调哪些方法。
这种"程序运行时审视和操作自己"的能力,就是反射(reflection)。
上一篇讲 struct tag 时就用到了反射去读 tag。这篇把 Go 的 reflect 包正经讲一遍:两个核心概念、怎么读、怎么改,以及什么时候别碰它。
反射到底在干嘛
正常写代码,类型是明确的:
var age int = 18
age = age + 1 // 编译器清楚 age 是 int,直接操作
但如果有个函数,参数是 interface{}(空接口,啥都能接),函数内部拿到的只是个"不知道具体类型的值",怎么办?
func printAny(v interface{}) {
// v 可能是 int、string、User 结构体... 运行时才知道
// 想知道它的类型、取它的字段,就得靠反射
}
反射就是解决这个的:在运行时动态地获取类型信息、读写值、调用方法。
JSON 序列化、ORM 把结构体映射成表、gin 绑定参数——这些框架不可能预先知道你的结构体长啥样,全靠反射在运行时现场分析。
两个核心:Type 和 Value
Go 反射的入口就两个函数,对应两个核心概念:
reflect.TypeOf(x):拿到 x 的类型信息(它是什么类型)。reflect.ValueOf(x):拿到 x 的值信息(它的值是多少,也能通过它改值)。
import "reflect"
func main() {
var age = 18
t := reflect.TypeOf(age) // 类型
v := reflect.ValueOf(age) // 值
fmt.Println("类型:", t) // int
fmt.Println("种类:", t.Kind()) // int(Kind 是底层种类)
fmt.Println("值:", v) // 18
fmt.Println("取回 int:", v.Int()) // 18(按 int 取出来)
}
这里有个容易混的点:Type 和 Kind。
Type是具体类型,比如你自定义的type MyInt int,Type 是MyInt。Kind是底层种类,MyInt的 Kind 还是int。
判断"这是不是个结构体""是不是个切片",要看 Kind(),不是 Type()。
反射读结构体
最常见的用法是遍历结构体的字段。
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func inspect(u interface{}) {
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
// 先确认传进来的确实是结构体,不然下面会 panic
if t.Kind() != reflect.Struct {
fmt.Println("不是结构体,溜了")
return
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i) // 第 i 个字段的类型信息
value := v.Field(i) // 第 i 个字段的值
// field.Tag.Get 读 tag,上一篇讲过
fmt.Printf("字段=%s 类型=%s 值=%v json标签=%s\n",
field.Name, field.Type, value, field.Tag.Get("json"))
}
}
func main() {
inspect(User{Name: "米虫", Age: 18})
}
输出:
字段=Name 类型=string 值=米虫 json标签=name
字段=Age 类型=int 值=18 json标签=age
NumField() 拿字段数量,Field(i) 取第 i 个,一遍循环就把整个结构体扒了个底朝天。这就是 ORM 把结构体字段对应到数据库列的基本原理。
反射改值的坑
反射不光能读,还能改值。但这里有个经典大坑,几乎人人栽过。
func main() {
age := 18
v := reflect.ValueOf(age)
v.SetInt(20) // panic!
}
直接这么写会 panic:reflect: reflect.Value.SetInt using unaddressable value。
原因:reflect.ValueOf(age) 传进去的是 age 的拷贝,改拷贝没意义,Go 直接禁止,所以报"不可寻址"。
想改原值,必须传指针进去,再用 Elem() 拿到指针指向的那个"可寻址"的值:
func main() {
age := 18
// 传指针 &age
v := reflect.ValueOf(&age)
// Elem() 解引用,拿到指针指向的可寻址 Value
elem := v.Elem()
// 还要确认它可被设置
if elem.CanSet() {
elem.SetInt(20)
}
fmt.Println(age) // 20,原值真的被改了
}
记住口诀:要改值,传指针,用 Elem,先 CanSet 再 Set。少一步都可能 panic。
反射调用方法
反射还能在运行时调用方法,连方法名都可以是字符串。
type Calc struct{}
func (c Calc) Add(a, b int) int { return a + b }
func main() {
c := Calc{}
v := reflect.ValueOf(c)
// 按名字拿到方法
method := v.MethodByName("Add")
// 参数要包成 []reflect.Value
args := []reflect.Value{reflect.ValueOf(3), reflect.ValueOf(5)}
// 调用,返回值也是 []reflect.Value
result := method.Call(args)
fmt.Println("Add(3,5) =", result[0].Int()) // 8
}
这种"用字符串名字调方法"的能力,是很多插件化、路由分发框架的底层手段。
什么时候别用反射
反射很强,但不是免费的,这几点要掂量:
- 慢:反射比直接代码慢一个数量级。热点路径、循环里大量用,性能会很难看。能用普通代码就别用反射。
- 丢编译期检查:类型错误从编译期挪到了运行期,本来编译能发现的 bug,变成线上 panic。
- 代码难读难维护:满屏
reflect.ValueOf、Field(i),可读性直线下降。
我的原则:写业务逻辑基本用不上反射,它是写框架、写通用库才需要的工具。日常被框架用就行,自己手写之前先问一句"真的非它不可吗"。
实在要用,常见的优化是把反射结果缓存起来(比如结构体字段信息解析一次存着),别每次都重新反射。json 包内部就是这么干的。
小结
反射是 Go 在运行时审视和操作自身类型与值的能力,入口是 reflect.TypeOf(取类型)和 reflect.ValueOf(取值)。
读结构体用 NumField + Field(i) 遍历;改值必须传指针、用 Elem()、先 CanSet 再 Set,否则 panic;还能用 MethodByName + Call 按名字调方法。
但反射慢、丢编译检查、难维护,它是框架和通用库的利器,不是业务代码的日常工具。能不用就不用,非用不可就把结果缓存起来。
搞懂它,JSON、ORM、gin 这些框架"凭空操作你结构体"的魔法,就都解释得通了。
