反射
2026年9月6日
反射能在运行时问一个值「你是什么类型、里面有什么」。入口是 reflect.TypeOf 和 reflect.ValueOf。框架、ORM、json 都靠它;业务代码能不用就不用。
类型判断
func kindOf(v any) {
t := reflect.TypeOf(v)
switch t.Kind() {
case reflect.Struct:
fmt.Println("结构体")
case reflect.Slice:
fmt.Println("切片")
case reflect.Map:
fmt.Println("map")
default:
fmt.Println(t.Kind())
}
}
go
Type 是名义类型(main.User),Kind 是底层种类(struct)。指针的 Kind 是 Ptr,要看指向什么用 t.Elem()。
通过反射获取值
v := reflect.ValueOf(18)
fmt.Println(v.Int())
fmt.Println(v.Interface())
go
按 Kind 选 Int / String / Bool……种类对不上会 panic。接口里的真实值用 Interface() 拿出来再断言。
通过反射修改值
要改外面的变量,必须传指针,再用 Elem() 走到指向的那块:
n := 1
v := reflect.ValueOf(&n).Elem()
if v.CanSet() {
v.SetInt(2)
}
fmt.Println(n) // 2
go
reflect.ValueOf(n) 拿到的是拷贝,CanSet() 是 false。
结构体反射
读字段和 json tag,没有 tag 就用字段名。下面没处理 - 和 omitempty:
func jsonKeys(v any) {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
key := f.Tag.Get("json")
if key == "" {
key = f.Name
}
fmt.Println(key)
}
}
go
修改结构体中某些值
比如 tag 里有 big,就把字符串字段改成大写:
func upperBig(p any) {
v := reflect.ValueOf(p).Elem()
t := v.Type()
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if t.Field(i).Tag.Get("big") == "" || !f.CanSet() {
continue
}
if f.Kind() == reflect.String {
f.SetString(strings.ToUpper(f.String()))
}
}
}
go
调用结构体方法
有名为 Call 的方法就调它:
m := reflect.ValueOf(u).MethodByName("Call")
if m.IsValid() {
m.Call(nil)
}
go
方法在指针接收者上,就要对指针做 ValueOf。
orm 的一个小案例
把结构体扫成「列名 = 值」,给拼 SQL 用:
type User struct {
Name string `db:"name"`
Age int `db:"age"`
}
func columns(p any) map[string]any {
v := reflect.ValueOf(p).Elem()
t := v.Type()
out := map[string]any{}
for i := 0; i < v.NumField(); i++ {
name := t.Field(i).Tag.Get("db")
if name == "" {
name = t.Field(i).Name
}
out[name] = v.Field(i).Interface()
}
return out
}
go
真 ORM 还要处理嵌套、主键、零值跳过,这里只演示反射怎么把字段掏出来。
对反射的一些建议
写框架、序列化、注入这类底层工具,不用反射很难。业务里大量反射不合适:
- 比直接代码慢一到两个数量级
- 读起来绕,出错推到运行时
- 编译期帮不上忙
能用接口、泛型、代码生成解决的,优先那些。