Golang反射如何做通用拷贝_Go语言对象复制实现

dee

pCopy 函数需分别处理指针、切片、映射和接口类型:指针需解引用后递归拷贝并新建指针;切片需创建新底层数组并逐元素拷贝;映射需新建并逐键值对递归拷贝;接口需先 Elem() 获取内部值,再判空避免 panic。

反射拷贝必须处理指针和非导出字段

Go 的 reflect 包默认跳过非导出(小写开头)字段,且对指针类型不做自动解引用。直接用 reflect.Copy 或遍历 reflect.Value 字段复制时,若源是 *T、目标是 T,或结构体含私有字段,会静默失败或 panic。

  • 确保源值已通过 reflect.Indirect 解引用到可读取的实际值,避免 panic: reflect: call of reflect.Value.Interface on zero Value
  • 目标值必须可寻址(reflect.Value.Addr() 可调用),否则无法写入;常见错误是传入字面量或只读副本
  • 非导出字段需用 reflect.StructField.IsExported() 显式跳过,不能依赖 CanInterface() 判断——它在字段不可导出时直接返回 false,但不报错

deepCopy 函数要区分 slice/map/interface{} 类型

通用拷贝不是简单递归调用 reflect.Copy。不同复合类型的复制逻辑差异大:slice 需新建底层数组,map 要重新 make 并逐键赋值,interface{} 值需先取出具体类型再处理。

  • reflect.Slice:用 reflect.MakeSlice(t, v.Len(), v.Cap()) 创建新 slice,再循环 SetMapIndexIndex(i).Set()
  • reflect.Map:必须先 reflect.MakeMapWithSize(t, v.Len()),再遍历 v.MapKeys(),对每个 key/value 递归 deepCopy 后 SetMapIndex
  • reflect.Interface:用 v.Elem() 获取内部值,再判断其 kind 是否为 reflect.Invalid(nil interface),避免 panic
func deepCopy(v reflect.Value) reflect.Value {
	if !v.IsValid() {
		return v
	}
	switch v.Kind() {
	case reflect.Ptr:
		if v.IsNil() {
			return reflect.Zero(v.Type())
		}
		unpacked := reflect.New(v.Elem().Type())
		unpacked.Elem().Set(deepCopy(v.Elem()))
		return unpacked
	case reflect.Slice:
		if v.IsNil() {
			return reflect.Zero(v.Type())
		}
		newSlice := reflect.MakeSlice(v.Type(), v.Len(), v.Cap())
		for i := 0; i < v.Len(); i++ {
			newSlice.Index(i).Set(deepCopy(v.Index(i)))
		}
		return newSlice
	case reflect.Map:
		if v.IsNil() {
			return reflect.Zero(v.Type())
		}
		newMap := reflect.MakeMapWithSize(v.Type(), v.Len())
		for _, key := range v.MapKeys() {
			newMap.SetMapIndex(key, deepCopy(v.MapIndex(key)))
		}
		return newMap
	default:
		if v.CanInterface() {
			return reflect.ValueOf(v.Interface())
		}
		return reflect.Zero(v.Type())
	}
}

struct 字段 tag 控制拷贝行为很实用

生产环境里常需要跳过某些字段(如数据库 ID、时间戳),或按需深拷贝(如嵌套 struct 是否展开)。靠反射自动推断不够可靠,应支持结构体字段 tag,例如 json:"-" 或自定义 copy:"-" / copy:"shallow"

  • 在遍历 struct 字段前,先检查 sf.Tag.Get("copy"),若为 "-" 则跳过该字段
  • 注意 tag 解析要 fallback 到空字符串,避免 Get 返回空导致误判;不要用 sf.Tag != "" 判断是否有 tag
  • 如果字段类型是 struct 且 tag 为 "shallow",则直接 Set 而非递归 deepCopy,减少开销

性能敏感场景别无脑用反射拷贝

反射拷贝比手写 Clone() 方法慢 10–100 倍,GC 压力也更大。尤其在高频调用(如 HTTP 中间件、序列化循环)中,容易成为瓶颈。

  • 基准测试显示:对含 5 个字段的 struct,反射 deepCopy 比手写方法慢约 40×;含嵌套 map 时差距扩大到 80×
  • 编译期生成代码(如 go:generate + goderive)比运行时反射更稳更快,适合核心模型
  • 如果只是浅拷贝且确定无指针别名风险,用 unsafe.Copy(Go 1.20+)或 bytes.Copy 配合 unsafe.Slice 更直接

真正难的不是写通反射逻辑,而是判断什么时候不该用它——比如字段带 mutex、channel、function 或 unsafe.Pointer,这些类型根本不能也不该被反射拷贝。