反射(reflection)
C++ 的 RTTI 只能拿到 typeid/dynamic_cast 少许信息;Go 的 reflect 能在运行时查看、遍历、修改任意类型与值 一、简述 反射(reflection):程序在运行时检查自身的类型与结构,并据此做动态操作。Go 里就是 reflect 包:给你 reflect.Type(类型信息)和 reflect.Value(一段具体值),能动态读字段、读 tag、调方法、甚至修改可设置的值。序列化库(encoding/json)、ORM、配置解析、测试断言统统建立在这套 API 之上。但反射有显著的性能代价与脆弱性——本章教你怎么用、什么时候值得用。 核心要点:反射的入口是 reflect.TypeOf / reflect.ValueOf,一切操作都围着这两个对象转;可设置性(CanSet) 是反射修值的前提(变量要可寻址);三大定律是官方给的使用边界。对 C++ 来说,typeid/dynamic_cast 只能做"类型识别",Go 反射还能"改值、建新值、动态调用",范围大得多,代价也大得多。 二、C++ 对照速查表 概念 C++ Go 关键差异 类型识别 typeid(x).name() reflect.TypeOf(x) C++ 只给名字,Go 给结构、字段、方法全图 运行时类型转换 dynamic_cast<T>(须多态) 首选类型断言 x.(T);reflect 侧有 Type.AssignableTo / ConvertibleTo / Implements 做判断、Value.Convert 做转换 Go 反射不要求继承,直接看类型;reflect 包没有 AssertableTo 方法 遍历字段 boost::pfr/手写 reflect.Value.NumField/Field Go 原生支持任意结构体遍历 读字段 tag 无标准机制 reflect.StructTag.Get Go 的 struct tag 是反射的主要消费场景 动态改值 *T + reinterpret_cast 危险操作 reflect.Value.Set* Go 有 CanSet 检查,越界即 panic 动态调用方法 std::invoke(C++17,仍要具体类型) reflect.Value.Method(i).Call(...) Go 无需编译期函数指针,纯运行时 创建新实例 T{} 写死 / 工厂模式 reflect.New(T) 反射可"凭空造值",C++ 做不到通用 性能 零开销(编译期类型) 每次反射调用开销 ~几百 ns 起 Go 反射充满动态分派与检查 应用场景 typeid 用于调试/序列化 序列化/ORM/测试/DI 反射生态在 Go 里是日常 类型断言 C++ 无 x.(T) 是内置语法 注意反射 TypeOf + 断言可以互相换算 三、逐主题详解 3.1 反射两大入口:Type 与 Value package main import ( "fmt" "reflect" ) type Person struct { Name string `json:"name"` Age int `json:"age"` } func main() { p := Person{Name: "小甲", Age: 18} t := reflect.TypeOf(p) // 类型信息 v := reflect.ValueOf(p) // 值信息 fmt.Println("Type:", t) // main.Person fmt.Println("Kind:", t.Kind()) // struct fmt.Println("Value:", v) // {小甲 18} fmt.Println("TypeOf(v):", v.Type()) // main.Person } reflect.Type:描述"类型是什么"——包名、名称、Kind、字段列表、方法集、是否可比较等。URI:t.NumField()、t.Field(i)、t.Method(i)。 reflect.Value:描述"这段值是什么"——包含了底层值,可读可设(受 CanSet 约束)、可调方法、可转 interface。 Kind():底层类别(Int、String、Slice、Struct、Map 等 26 种)。Type 是具体类型(main.Person),Kind 是粗类别(struct)——C++ 里没有直接对应,最接近 typeid 得不到 Kind;Go 里 Kind 用于判断"数据结构形状"。 3.2 从 Value 取回具体值:Interface() v := reflect.ValueOf(p) // 把 Value 还原成 interface{} i := v.Interface() // 再断言回具体类型 p2 := i.(Person) fmt.Println(p2.Name) // 小甲 老代码里 v.Interface() + 类型断言是组合拳;Go 1.x 有 v.CanInterface() 检查(未导出字段、flag 受限时不可 Interface())。 原则:反射只是"绕到类型系统后面看一眼",看完一定要回到具体类型继续干活。 3.3 对 struct 字段遍历与 tag 读取(json 的底层原理) func dumpStruct(s any) { v := reflect.ValueOf(s) t := v.Type() fmt.Printf("类型: %s\n", t) for i := 0; i < v.NumField(); i++ { field := t.Field(i) // reflect.StructField val := v.Field(i) // reflect.Value tag := field.Tag.Get("json") // 读 tag fmt.Printf(" 字段 %s kind=%v 值=%v tag=%q\n", field.Name, field.Type.Kind(), val.Interface(), tag) } } dumpStruct(Person{Name: "张三", Age: 22}) // 输出: // 类型: main.Person // 字段 Name kind=string 值=张三 tag="name" // 字段 Age kind=int 值=22 tag="age" 这就是 encoding/json 的底层机制:遍历 struct 字段 → 看 json:"xxx" tag 决定键名/跳过 → 按字段类型编码/解码。 StructTag.Get("json") 可以返回 name,omitempty 这样的组合串(Lookup 可区分"有但空"和"没有")。 字段可以是导出(大写)或未导出(小写);未导出字段反射读值会 panic,写值 CanSet=false。 3.4 可设置性(CanSet)与修改值 反射改值前提:值必须是可以寻址的(addressable)。reflect.ValueOf(p) 拿到的是拷贝的 p——改它改不到原对象。 ...
豫公网安备41108202000982号