需要预先了解:


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 转换只有四种:

  1. 任意类型的指针 → unsafe.Pointer:
    p := (*int)(unsafe.Pointer(&x))
  2. unsafe.Pointer → 任意类型的指针;
  3. uintptr → unsafe.Pointer;
  4. 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 练习

  1. 写一个 Validate(v any) error,用反射检查每个带 required tag 的字符串字段是否为空,为空则返回错误。
  2. 用反射实现 SetField(v any, name string, value any) error,支持按字段名和 tag 名查找,类型不匹配时返回错误。
  3. 用 unsafe.Sizeof 和 unsafe.Offsetof 打印一个含 int8、int64、bool 的结构体的字段布局,再用反射打印字段类型,对比两种方式得到的信息。