Files
reflux/rfx_generic.go
T
what 9cb3ea857e fix: 补齐 Get[T] 的类型分支,避免静默返回零值
Get[T] 原本只处理 string/int/int64/bool/float64 五种,其余类型掉进 default
分支的 Any().(T) 断言。而断言**不做转换**,导致:

  Get[int32](r, "Age")            // 字段是 int(42) -> 静默返回 0
  Get[[]string](r, "Tags")        // 字段是 []any   -> 静默返回 nil
  Get[map[string]string](r, "M")  // 字段是 map[string]any -> 静默返回 nil

而对应的访问器 r.Get("Age").Int32() 等返回的是正确的转换结果。
实测 8 个场景里 7 个静默给错数据 —— 不报错不 panic,是最难排查的一类问题。

补齐后 T 覆盖 valuex.Accessor 全部转换方法对应的类型(9 个标量 + 4 种切片
+ 6 种 map),每个分支都落到同名访问器上,转换语义完全一致。
五种快路径类型的零分配特性不受影响(新分支同样用 detachPath 切断逃逸)。

新增 TestGenericMirrorsAccessors: 23 个用例逐个比对 Get[T] 与同名访问器,
并额外断言结果不是零值,防止将来有人删掉分支又掉回 default。
2026-08-28 16:50:43 +08:00

135 lines
4.7 KiB
Go

package reflux
// Get 按路径取值并直接转成目标类型 T,不产生中间的 R 包装对象。
//
// name := reflux.Get[string](r, "Address", "City")
// age := reflux.Get[int](r, "Age")
// tags := reflux.Get[[]string](r, "Tags")
//
// 语义与 r.Get(path...).Xxx() 严格等价,是可以放心替换的写法:
// - 路径不存在 / 未导出字段 / 下标越界 -> 返回 T 的零值
// - 类型转换失败 -> panic,错误信息与访问器方法一致
//
// T 覆盖 valuex.Accessor 全部转换方法对应的类型(见下面的 switch),
// 每一个分支都落到与之同名的访问器上,因此转换语义完全一致 ——
// 比如 Get[[]string] 读一个 []any 字段时会逐元素转换,而不是类型断言失败返回 nil。
//
// 其中 string / int / int64 / bool / float64 五种走零分配快路径:
// 不构造中间的 R 包装对象,直接把结果写进调用方的变量。其余类型的结果本身
// 就要分配(切片、map),没有额外快路径可言,但语义一样正确。
//
// 两点注意:
//
// 1. T 只出现在返回值里,Go 无法类型推导,必须显式写出 Get[string](...)。
// 2. 列表之外的类型走 default 分支的 Any().(T) 断言 —— 这对
// Get[SomeStruct](r, "Field") 这种"取出原样的值"是有用的,但**不做转换**:
// 具名标量类型(type MyStr string)在字段是原生 string 时会断言失败返回零值,
// 这种场景请改用 r.Get("Name").String()。
func Get[T any](r R, path ...string) T {
var out T
// 用 any(&out) 而不是 any(out) 做类型分发: 前者装箱的是指针,
// 指针本身就是接口的数据字段,不需要额外堆分配;后者会把值拷进堆。
switch p := any(&out).(type) {
// ---- 零分配快路径: 命中 *rfx 时直接读内存,不建包装对象 ----
case *string:
if g, ok := r.(*rfx); ok {
*p = g.getString(path...)
} else {
*p = r.Get(detachPath(path)...).String()
}
case *int:
if g, ok := r.(*rfx); ok {
*p = g.getInt(path...)
} else {
*p = r.Get(detachPath(path)...).Int()
}
case *int64:
if g, ok := r.(*rfx); ok {
*p = g.getInt64(path...)
} else {
*p = r.Get(detachPath(path)...).Int64()
}
case *bool:
if g, ok := r.(*rfx); ok {
*p = g.getBool(path...)
} else {
*p = r.Get(detachPath(path)...).Bool()
}
case *float64:
if g, ok := r.(*rfx); ok {
*p = g.getFloat64(path...)
} else {
*p = r.Get(detachPath(path)...).Float64()
}
// ---- 其余标量: 没有零分配快路径,但必须走对应访问器以保证转换语义 ----
//
// 这些分支不能省。少了它们就会掉进 default 的 Any().(T) 断言,
// 而断言是不做转换的 —— Get[int32] 读一个 int 字段会**静默返回 0**,
// 而 r.Get(...).Int32() 返回的是正确的值。静默给错数据比 panic 更难查。
case *int8:
*p = r.Get(detachPath(path)...).Int8()
case *int16:
*p = r.Get(detachPath(path)...).Int16()
case *int32:
*p = r.Get(detachPath(path)...).Int32()
case *uint:
*p = r.Get(detachPath(path)...).Uint()
case *uint8:
*p = r.Get(detachPath(path)...).Uint8()
case *uint16:
*p = r.Get(detachPath(path)...).Uint16()
case *uint32:
*p = r.Get(detachPath(path)...).Uint32()
case *uint64:
*p = r.Get(detachPath(path)...).Uint64()
case *float32:
*p = r.Get(detachPath(path)...).Float32()
// ---- 切片 ----
case *[]any:
*p = r.Get(detachPath(path)...).Slice()
case *[]string:
*p = r.Get(detachPath(path)...).StringSlice()
case *[]int:
*p = r.Get(detachPath(path)...).IntSlice()
case *[]bool:
*p = r.Get(detachPath(path)...).BoolSlice()
// ---- map ----
case *map[string]any:
*p = r.Get(detachPath(path)...).StringMap()
case *map[string]string:
*p = r.Get(detachPath(path)...).StringMapString()
case *map[string]int:
*p = r.Get(detachPath(path)...).StringMapInt()
case *map[string]int64:
*p = r.Get(detachPath(path)...).StringMapInt64()
case *map[string]bool:
*p = r.Get(detachPath(path)...).StringMapBool()
case *map[string][]string:
*p = r.Get(detachPath(path)...).StringMapStringSlice()
default:
// 不在上面列表里的类型: 取出原样的值做断言,不做转换。
if x, ok := r.Get(detachPath(path)...).Any().(T); ok {
out = x
}
}
return out
}
// detachPath 复制一份路径切片。
//
// 这一步看着多余,实则必要: 上面那些 r.Get(...) 是**接口动态调用**,逃逸分析
// 看不穿,于是判定 path 整体逃逸 —— 哪怕运行时走的是零分配快分支。
// 在慢分支里复制一份切断数据流,快分支的可变参数才能留在栈上。
// 少了这一步,Get[string] 会从 0 分配退化成 1 次 48 字节分配。
func detachPath(p []string) []string {
q := make([]string, len(p))
copy(q, p)
return q
}