Go 1.18前用interface{}+reflect实现伪泛型,代价是性能损耗、类型不安全和调试困难;需用Kind()判类型、Set()赋值(目标须可寻址)、Get()解析tag、缓存struct信息优化性能。

Go反射怎么替代泛型写通用函数
Go 1.18 之前没有泛型,开发者普遍用 interface{} + reflect 包实现“伪泛型”逻辑。这不是语法糖,而是运行时类型擦除后的补救方案——它能工作,但代价明确:性能损耗、类型安全丢失、调试困难。
典型场景是写一个通用的 DeepCopy、MapToStruct 或 Compare 函数。这类函数不关心具体类型,只依赖结构共性(字段名、可导出性、嵌套关系)。
- 必须用
reflect.ValueOf(x).Kind()判断底层类型,不能只靠reflect.TypeOf(x).Name() - 所有赋值操作必须通过
reflect.Value.Set(),且目标Value必须是可寻址的(reflect.Value.Addr()或传指针) - 对非导出字段(小写开头)无法读写,
reflect会静默失败或 panic
为什么 reflect.Value.Elem() 经常 panic
Elem() 用于解引用指针、切片、映射、通道、接口等类型的底层值。最常见错误是没检查是否可调用就直接调用——比如对 int 类型调 .Elem(),或对 nil 指针调用。
正确做法是先用 v.Kind() 判断类型,再按需处理:
func safeElem(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Ptr && !v.IsNil() {
return v.Elem()
}
if v.Kind() == reflect.Interface && v.IsNil() == false {
return v.Elem()
}
return v
}
-
v.Kind() == reflect.Ptr且v.IsNil()为true时,v.Elem()panic -
v.Kind() == reflect.Interface且内部值为nil,同样触发 panic - 不要假设输入一定是指针;通用函数必须兼容值和指针两种传参方式
struct tag 解析必须配合 reflect.StructTag.Get
反射中读取 struct 字段 tag(如 json:"name,omitempty")不能直接访问 Field.Tag 字符串,而要用 Field.Tag.Get("json")。否则会拿到原始字符串,还得自己 parse,极易出错。
更关键的是:tag 值可能为空(json:"-" 表示忽略),或含选项(omitempty),Get 方法自动处理这些边界:
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "-" || jsonTag == "" {
continue
}
// 解析 name 和 options
name, opts, _ := strings.Cut(jsonTag, ",")
if slices.Contains(opts, "omitempty") && isEmpty(value) {
continue
}
-
Field.Tag是reflect.StructTag类型,不是string;直接转字符串会丢失结构化语义 -
Get返回空字符串表示未设置该 tag,不是解析失败 - 别在反射循环里反复调用
strings.Split手动解析,性能差且易漏omitempty等选项
反射性能差在哪?怎么缓存缓解
每次调用 reflect.TypeOf 或 reflect.ValueOf 都触发运行时类型查找和封装,开销远高于普通函数调用。真正拖慢的不是反射本身,而是重复构建 reflect.Type 和遍历字段树。
可行的优化是按类型缓存字段信息(如字段索引、是否导出、tag 解析结果):
var typeCache sync.Map // map[reflect.Type]*structInfo
type structInfo struct {
Fields []fieldInfo
}
type fieldInfo struct {
Index int
Name string
Tag string
IsPtr bool
}
func getStructInfo(t reflect.Type) *structInfo {
if cached, ok := typeCache.Load(t); ok {
return cached.(*structInfo)
}
info := &structInfo{}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.IsExported() {
continue
}
info.Fields = append(info.Fields, fieldInfo{
Index: i,
Name: f.Name,
Tag: f.Tag.Get("json"),
IsPtr: f.Type.Kind() == reflect.Ptr,
})
}
typeCache.Store(t, info)
return info
}
- 缓存 key 必须是
reflect.Type,不是string(避免同名不同包冲突) - 不要缓存
reflect.Value,它包含运行时状态,不可复用 - 首次调用仍慢,但后续同类型操作快一个数量级;对高频调用的通用函数值得加
反射不是银弹,它把编译期检查移到了运行时。哪怕用了缓存,也掩盖不了类型不安全的本质——这也是 Go 团队坚持等泛型落地的原因。现在回头看,那些满屏 reflect.Value.Kind() 嵌套的代码,基本都在等 func[T any] 来重写。










