Go标准库未提供reflect.DeepCopy是设计使然,因深拷贝语义(如指针处理、字段跳过、循环引用)需用户定义;反射仅暴露结构信息,不负责内存复制逻辑。

为什么 reflect.DeepCopy 不存在
Go 标准库没有提供 reflect.DeepCopy 函数,也不是遗漏——而是设计上刻意回避。反射本身不负责内存复制逻辑,reflect 包只暴露类型和值的结构信息,真正的拷贝行为必须由用户定义语义:是否复制指针指向的内容?是否跳过某些字段(如 sync.Mutex)?是否处理循环引用?这些都无法由反射自动推断。
用 reflect.Value.Set + 递归实现基础深拷贝
核心思路是:对源值递归遍历每个字段或元素,为每个目标位置创建新实例,再用 reflect.Value.Set 赋值。但必须注意类型可寻址性、不可设置字段(如未导出字段、func、unsafe.Pointer)、以及非复合类型的直接复制。
常见错误现象:panic: reflect: reflect.Value.Set using unaddressable value —— 源值不是地址,或目标值未通过 reflect.New 或 reflect.MakeSlice 等获取可寻址副本。
- 只处理导出字段(未导出字段无法通过反射写入,强行访问会 panic)
- 遇到
nil指针、切片、map,需先用reflect.New/reflect.MakeSlice/reflect.MakeMap初始化目标 - 对
interface{}类型需先取底层值(v.Elem()),再递归处理 - 原始类型(
int、string、bool)直接Set即可,无需额外分配
// 示例:简单结构体深拷贝(无嵌套未导出字段、无循环引用)
func deepCopy(src interface{}) interface{} {
v := reflect.ValueOf(src)
if !v.IsValid() {
return nil
}
dst := reflect.New(v.Type()).Elem()
copyValue(v, dst)
return dst.Interface()
}
func copyValue(src, dst reflect.Value) {
switch src.Kind() {
case reflect.Ptr:
if src.IsNil() {
dst.Set(reflect.Zero(dst.Type()))
return
}
dstPtr := reflect.New(src.Elem().Type())
copyValue(src.Elem(), dstPtr.Elem())
dst.Set(dstPtr)
case reflect.Struct:
for i := 0; i < src.NumField(); i++ {
if src.Type().Field(i).IsExported() {
copyValue(src.Field(i), dst.Field(i))
}
}
case reflect.Slice, reflect.Array:
newSlice := reflect.MakeSlice(dst.Type(), src.Len(), src.Cap())
for i := 0; i < src.Len(); i++ {
copyValue(src.Index(i), newSlice.Index(i))
}
dst.Set(newSlice)
case reflect.Map:
newMap := reflect.MakeMap(dst.Type())
for _, key := range src.MapKeys() {
val := reflect.New(src.MapIndex(key).Type()).Elem()
copyValue(src.MapIndex(key), val)
newMap.SetMapIndex(key, val)
}
dst.Set(newMap)
default:
dst.Set(src)
}
}
什么时候不该用反射做深拷贝
反射深拷贝性能差、易出错、难以调试,仅适用于「类型不确定且无法预知结构」的极少数场景。大多数情况下应优先选择:
立即学习“go语言免费学习笔记(深入)”;
- 实现
Clone() interface{}方法(显式、可控、零反射开销) - 使用
encoding/gob或encoding/json序列化+反序列化(但要求字段可导出,且会丢失方法、通道、函数等) - 第三方库如
github.com/jinzhu/copier(支持标签控制、忽略字段、自定义转换,但仍有反射成本)
特别注意:json.Marshal/Unmarshal 会把 time.Time 变成字符串再还原,gob 不跨进程兼容,而反射方案对 sync.Mutex、chan、func 等类型直接 panic,必须提前过滤。
未导出字段和循环引用的真实限制
反射无法绕过 Go 的可见性规则。即使你用 unsafe 强行读取未导出字段,也无法安全写入——运行时会拒绝设置,或导致内存损坏。循环引用则更棘手:递归无终止条件时栈溢出,加缓存(map[uintptr]reflect.Value)又无法正确处理指针地址变化(如不同 goroutine 中同一对象地址可能不同)。
这意味着:任何声称“通用、安全、支持循环引用”的反射深拷贝实现,要么做了妥协(如跳过未导出字段、限制最大深度),要么隐藏了不可靠行为。实际项目中,深拷贝逻辑越贴近业务数据结构,越稳定。










