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 }