反射与 unsafe
需要预先了解:
1. 反射与 unsafe
反射让程序在运行时检查类型、读取字段、调用方法;unsafe 则允许绕过类型系统直接操作内存。两者都是"不得已才用"的工具:反射带来灵活性和性能代价,unsafe 带来性能和不安全性。
1.1 为什么需要反射
编译期类型已知时,直接写代码就行。但有些场景类型只有在运行时才知道:
encoding/json要把任意 struct 转成 JSON,它不可能为每个类型写一份代码;- ORM 要根据结构体字段生成 SQL;
- 配置加载要按 tag 把 YAML 映射到字段。
这些场景都需要"把类型信息当作数据来操作",这就是反射。
1.2 反射三定律
第一定律:接口值可以拆成反射对象。
var x any = User{Name: "一撮灰"}
t := reflect.TypeOf(x) // 类型信息
v := reflect.ValueOf(x) // 值
第二定律:反射对象可以还原成接口值。
back := v.Interface().(User)
Interface() 返回 any,需要断言才能取回具体类型。
第三定律:要修改值,反射对象必须可寻址(CanSet)。
u := User{Name: "一撮灰"}
v := reflect.ValueOf(u)
fmt.Println(v.Field(0).CanSet()) // false
vp := reflect.ValueOf(&u)
fmt.Println(vp.Elem().Field(0).CanSet()) // true
vp.Elem().Field(0).SetString("灰灰")
直接取值的 CanSet: false
指针 Elem 后的 CanSet: true
修改后: {灰灰 4 内部}
原因是:reflect.ValueOf(u) 传的是值的副本,改它没有意义;ValueOf(&u).Elem() 指向原变量,才可写。
1.3 reflect.Type 与 reflect.Value
Type 描述类型,Value 描述值。两者最容易混淆的是 Kind 和 Type 的区别:
type MyInt int
var i int = 1
var m MyInt = 2
fmt.Println(reflect.TypeOf(i), reflect.TypeOf(i).Kind()) // int int
fmt.Println(reflect.TypeOf(m), reflect.TypeOf(m).Kind()) // main.MyInt int
Kind 是底层种类(int、struct、slice、ptr…),Type 是完整类型(包括名字)。判断"是不是结构体"用 Kind,判断"是不是某个具体类型"用 Type。
遍历结构体字段:
t := reflect.TypeOf(u)
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Printf("%-8s 类型=%-6v tag=%-22q 导出=%v\n", f.Name, f.Type, f.Tag, f.IsExported())
}
Name 类型=string tag="json:\"name\"" 导出=true
Age 类型=int tag="json:\"age,omitempty\"" 导出=true
private 类型=string tag="" 导出=false
IsExported() 判断字段是否导出——未导出字段的 Value 不能 Interface(),会 panic。
1.4 结构体 tag 解析
结构体与指针 里预告过的 tag,在反射里就是一段普通字符串:
f, _ := t.FieldByName("Age")
fmt.Println(f.Tag) // json:"age,omitempty"
fmt.Println(f.Tag.Get("json")) // age,omitempty
StructTag.Get(key) 按 key:"value" 的语法解析,找不到返回空串。多个 tag 用空格分隔。
标准库的 encoding/json、yaml、validate 都是这样读取 tag 的,见 标准库(二):JSON 与 HTTP 服务。
1.5 动态调用方法
g := &Greeter{Name: "一撮灰"}
gv := reflect.ValueOf(g)
gv.MethodByName("SetName").Call([]reflect.Value{reflect.ValueOf("新名字")})
result := gv.MethodByName("Greet").Call(nil)
fmt.Println(g.Name, "->", result[0].String())
新名字 -> 你好,新名字
Call 的参数和返回值都是 []reflect.Value,参数类型不匹配会在运行时 panic。MethodByName 只能找到导出方法。
1.6 反射的性能代价
BenchmarkDirectField-24 1000000000 0.2274 ns/op
BenchmarkReflectCached-24 953657540 0.3838 ns/op
BenchmarkReflectLookup-24 10041924 36.29 ns/op
- 直接访问字段:0.23ns;
- 提前缓存好
reflect.Value再读:0.38ns(几乎无差别); - 每次都
ValueOf + FieldByName查找:36ns,慢 150 倍以上。
结论很明确:反射的慢主要在"查找",不在"读取"。如果反射逻辑在热点路径上,把 reflect.Type/reflect.Value 缓存起来能省掉绝大部分开销。encoding/json 就是这么做的——它为每个类型缓存一份编解码计划。
1.7 实战:struct 转 map
一个 40 行的版本,把导出字段按 tag 名字转成 map:
func ToMap(v any) map[string]any {
out := map[string]any{}
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
if rv.Kind() != reflect.Struct {
return out
}
rt := rv.Type()
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
if !f.IsExported() {
continue
}
name := f.Tag.Get("json")
if name == "" {
name = f.Name
}
out[name] = rv.Field(i).Interface()
}
return out
}
o := Order{ID: 1, Items: []string{"书", "笔"}, Total: 42.5, internal: "隐藏"}
fmt.Println(ToMap(o))
fmt.Println(ToMap(&o))
fmt.Println(ToMap(42))
map[id:1 items:[书 笔] total:42.5]
map[id:1 items:[书 笔] total:42.5]
map[]
处理指针、跳过未导出字段、读取 tag——这就是一个迷你序列化器的骨架。
1.8 unsafe.Pointer 的四条规则
Go 规范允许的 unsafe.Pointer 转换只有四种:
- 任意类型的指针 →
unsafe.Pointer:p := (*int)(unsafe.Pointer(&x)) unsafe.Pointer→ 任意类型的指针;uintptr→unsafe.Pointer;unsafe.Pointer→uintptr。
规则 3 和 4 的组合就是指针算术:
p := unsafe.Pointer(&s[0])
q := unsafe.Pointer(uintptr(p) + 8) // 跳到下一个元素
关键约束:uintptr 只是一个整数,GC 不认为它引用了对象。如果把它保存下来跨越一次 GC,对象可能已经被移动或回收,再转回指针就会得到悬空地址:
u := uintptr(unsafe.Pointer(&x))
runtime.GC() // 这期间 x 可能被移动/回收
p := (*int)(unsafe.Pointer(u)) // 危险!
正确写法是把 unsafe.Pointer → uintptr → unsafe.Pointer 写在同一个表达式里,中间不要保存 uintptr。
1.9 零拷贝转换
Go 1.20 引入了 unsafe.String 和 unsafe.SliceData,让 string 与 []byte 的转换不再需要手写头部结构:
func StringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
func BytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
s := "hello"
b := StringToBytes(s)
fmt.Println(string(b), unsafe.StringData(s) == unsafe.SliceData(b)) // hello true
注意:通过这种方式得到的 []byte 和原字符串共享内存,而字符串数据是只读的,绝对不能写。写入会破坏只读段,可能导致段错误或难以排查的 bug。
旧写法(*(*[]byte)(unsafe.Pointer(&s)))依赖字符串头的内存布局,可读性差且容易出错,新代码不应该再用。
1.10 reflect 与泛型的互补
有了泛型,还需要反射吗?需要,两者解决的不是同一类问题:
| 场景 | 推荐 |
|---|---|
| 容器、算法,类型在编译期已知 | 泛型 |
| 需要按字段名/tag 遍历、动态构造类型 | 反射 |
| 需要根据运行时字符串找方法 | 反射(MethodByName) |
| 需要在运行时知道"这个值是什么类型" | 反射(TypeOf) |
encoding/json 之所以还要用反射,是因为调用方传进来的类型在库编译时完全未知,只能运行时分析。而 slices.Sort 能用泛型,是因为"排序一个 []T"的逻辑与 T 无关,编译期就能特化。
注:反射的代码尽量限制在库的边界上——入口处解析一次类型,转换成具体逻辑,之后的业务代码全部用具体类型。不要在业务逻辑里到处用
reflect.Value。
1.11 练习
- 写一个
Validate(v any) error,用反射检查每个带requiredtag 的字符串字段是否为空,为空则返回错误。 - 用反射实现
SetField(v any, name string, value any) error,支持按字段名和 tag 名查找,类型不匹配时返回错误。 - 用
unsafe.Sizeof和unsafe.Offsetof打印一个含int8、int64、bool的结构体的字段布局,再用反射打印字段类型,对比两种方式得到的信息。



