Golang中的深浅拷贝

152 阅读6分钟

引言

大家好,我是二毛。

在 Go 要对变量 b 进行深度拷贝,以确保修改 b 不会影响原始变量 a,需要确保在复制 b 的过程中,对其中的所有嵌套引用类型的数据(如切片、map、指针、结构体)都进行递归复制,而不是仅仅复制指针或引用。

1. 什么是深度拷贝

深度拷贝意味着复制变量及其所有嵌套的值,而不仅仅是复制指针。换句话说,深拷贝后,两个变量之间没有共享的内存地址。相反,浅拷贝只是复制变量的引用或指针,导致两个变量共享相同的底层数据。

在 Go 语言中,通常需要手动实现深拷贝,因为标准库并没有提供通用的深拷贝函数。下面我们通过一个实际例子来说明如何对不同类型的数据进行深拷贝。

2. 手动深度拷贝结构体

假设你有一个结构体 Person,结构体内部有一些嵌套的引用类型(如切片或 map):

type Address struct {
    City    string
    ZipCode string
}

type Person struct {
    Name    string
    Age     int
    Hobbies []string
    Addr    *Address
}

你想要对 Person 进行深度拷贝,使得修改 b 后不会影响 a

3. 优化代码实现深度拷贝

方案一:手动实现深度拷贝

对于结构体中的每个字段,你都需要递归地进行拷贝。例如:

func deepCopyPerson(a Person) Person {
    // 复制简单类型字段
    b := a // 浅拷贝

    // 复制切片(深拷贝)
    if a.Hobbies != nil {
        b.Hobbies = make([]string, len(a.Hobbies))
        copy(b.Hobbies, a.Hobbies)
    }

    // 复制指针类型(深拷贝)
    if a.Addr != nil {
        b.Addr = &Address{
            City:    a.Addr.City,
            ZipCode: a.Addr.ZipCode,
        }
    }

    return b
}

4. 更复杂的数据结构深拷贝

对于更复杂的数据结构(例如包含嵌套 map、切片等),你需要根据每种类型手动进行处理。如果数据结构非常复杂,手动深度拷贝可能会导致代码冗长且易出错。

方案二:使用序列化和反序列化进行深拷贝

另一种实现深度拷贝的通用方法是通过序列化(如 JSON)和反序列化。虽然这种方法效率相对较低,但在处理复杂结构时非常简便。你可以先将结构体序列化为 JSON,再反序列化回一个新的变量,从而实现深度拷贝。

import (
    "encoding/json"
    "log"
)

func deepCopyUsingJSON(a Person) Person {
    var b Person
    jsonData, err := json.Marshal(a)
    if err != nil {
        log.Fatalf("Error during marshaling: %v", err)
    }

    err = json.Unmarshal(jsonData, &b)
    if err != nil {
        log.Fatalf("Error during unmarshaling: %v", err)
    }

    return b
}

5. 示例对比

func main() {
    a := Person{
        Name:    "Alice",
        Age:     30,
        Hobbies: []string{"Reading", "Hiking"},
        Addr:    &Address{City: "New York", ZipCode: "10001"},
    }

    // 方案一:手动深度拷贝
    b := deepCopyPerson(a)

    // 方案二:通过JSON进行深度拷贝
    c := deepCopyUsingJSON(a)

    // 修改 b 的值
    b.Hobbies[0] = "Swimming"
    b.Addr.City = "San Francisco"

    // 修改 c 的值
    c.Hobbies[1] = "Gaming"
    c.Addr.ZipCode = "94102"

    // 输出 a、b、c,验证 a 没有受到 b、c 的影响
    fmt.Println("a:", a)
    fmt.Println("b:", b)
    fmt.Println("c:", c)
}

输出结果显示 a 并没有受到 bc 的修改影响:

a: {Alice 30 [Reading Hiking] &{New York 10001}}
b: {Alice 30 [Swimming Hiking] &{San Francisco 10001}}
c: {Alice 30 [Reading Gaming] &{New York 94102}}

6. 总结

  • 手动深度拷贝可以根据不同数据类型精准控制,但可能较繁琐。
  • 使用 JSON 序列化和反序列化是一种通用且简单的深拷贝方式,适合较复杂的嵌套结构,但可能存在一定性能损耗。
  • 你可以根据需求选择不同的实现方式。如果追求性能,建议手动实现;如果数据结构复杂且追求简洁代码,JSON 序列化是一种可行的方案。

问题 1:实战,完成方法实现深拷贝二维切片

手动实现深拷贝

package main

import "fmt"

func deepCopy2DSlice(a [][]int) [][]int {
    // 创建一个新的二维切片,长度和 a 一致
    b := make([][]int, len(a))
    for i := range a {
        // 为每个子切片分配足够的空间
        b[i] = make([]int, len(a[i]))
        // 使用内置的 copy 函数进行深度拷贝
        copy(b[i], a[i])
    }
    return b
}

func main() {
    var a = [][]int{{1}, {2}, {3}}
    fmt.Println("Original a:", a)

    // 调用深度拷贝函数
    b := deepCopy2DSlice(a)

    // 修改 b 的值,不影响 a
    b[0][0] = 11

    // 打印结果,a 和 b 应该不同
    fmt.Println("After modifying b:")
    fmt.Println("a:", a)
    fmt.Println("b:", b)
}

问题 2:能不能写一个更加通用的函数,能够深拷贝 一维切片 二维切片,map 结构体变量指针引用 等

编写一个通用的深度拷贝函数,能够支持各种数据类型(如一维切片、二维切片、map、结构体、指针引用等),可以通过使用 反射(reflect) 来处理 Go 中的动态类型。在 Go 中,反射可以用来查看和操作未知类型的数据结构。

package main

import (
	"fmt"
	"reflect"
)

func deepCopy(src interface{}) interface{} {
	if src == nil {
		return nil
	}

	// 获取 src 的反射值
	srcValue := reflect.ValueOf(src)
	srcType := reflect.TypeOf(src)

	// 根据 src 的类型,进行深度拷贝
	switch srcType.Kind() {
	case reflect.Ptr: // 如果是指针,递归拷贝指向的值
		if srcValue.IsNil() {
			return nil
		}
		newPtr := reflect.New(srcType.Elem())
		newPtr.Elem().Set(reflect.ValueOf(deepCopy(srcValue.Elem().Interface())))
		return newPtr.Interface()

	case reflect.Slice: // 如果是切片
		if srcValue.IsNil() {
			return nil
		}
		newSlice := reflect.MakeSlice(srcType, srcValue.Len(), srcValue.Cap())
		for i := 0; i < srcValue.Len(); i++ {
			newSlice.Index(i).Set(reflect.ValueOf(deepCopy(srcValue.Index(i).Interface())))
		}
		return newSlice.Interface()

	case reflect.Map: // 如果是 map
		if srcValue.IsNil() {
			return nil
		}
		newMap := reflect.MakeMap(srcType)
		for _, key := range srcValue.MapKeys() {
			// 深度拷贝 map 中的键和值
			newValue := reflect.ValueOf(deepCopy(srcValue.MapIndex(key).Interface()))
			// 使用 SetMapIndex 来设置键值对
			newMap.SetMapIndex(key, newValue)
		}
		return newMap.Interface()

	case reflect.Struct: // 如果是结构体
		newStruct := reflect.New(srcType).Elem()
		for i := 0; i < srcType.NumField(); i++ {
			fieldValue := srcValue.Field(i)
			if fieldValue.CanSet() {
				newStruct.Field(i).Set(reflect.ValueOf(deepCopy(fieldValue.Interface())))
			}
		}
		return newStruct.Interface()

	default: // 如果是基本类型(int, string, bool等),直接返回值
		return src
	}
}

func main() {
	// 测试一维切片
	a := []int{1, 2, 3}
	b := deepCopy(a).([]int)
	b[0] = 10
	fmt.Println("Slice test:")
	fmt.Println("a:", a)
	fmt.Println("b:", b)

	// 测试二维切片
	c := [][]int{{1, 2}, {3, 4}}
	d := deepCopy(c).([][]int)
	d[0][0] = 10
	fmt.Println("\n2D Slice test:")
	fmt.Println("c:", c)
	fmt.Println("d:", d)

	// 测试 map
	e := map[string]int{"key1": 1, "key2": 2}
	f := deepCopy(e).(map[string]int)
	f["key1"] = 10
	fmt.Println("\nMap test:")
	fmt.Println("e:", e)
	fmt.Println("f:", f)

	// 测试结构体
	type Person struct {
		Name string
		Age  int
	}
	g := Person{"Alice", 30}
	h := deepCopy(g).(Person)
	h.Name = "Bob"
	fmt.Println("\nStruct test:")
	fmt.Println("g:", g)
	fmt.Println("h:", h)

	// 测试指针
	i := &Person{"Charlie", 25}
	j := deepCopy(i).(*Person)
	j.Name = "Dave"
	fmt.Println("\nPointer test:")
	fmt.Println("i:", i)
	fmt.Println("j:", j)
}

输出:

Slice test:
a: [1 2 3]
b: [10 2 3]

2D Slice test:
c: [[1 2] [3 4]]
d: [[10 2] [3 4]]

Map test:
e: map[key1:1 key2:2]
f: map[key1:10 key2:2]

Struct test:
g: {Alice 30}
h: {Bob 30}

Pointer test:
i: &{Charlie 25}
j: &{Dave 25}

解释

  1. 反射机制:通过 reflect 包,我们可以动态地处理各种数据类型。reflect.ValueOf 返回一个包含 src 值的 reflect.Value,并使用 reflect.TypeOf 获取其类型。
  2. 类型检查:通过 srcType.Kind() 判断 src 是哪种类型,针对不同的类型进行处理:
    • 指针:递归拷贝指向的值。
    • 切片:新建一个切片,并递归拷贝每个元素。
    • map:新建一个 map,并递归拷贝每个键值对。
    • 结构体:新建一个结构体,并递归拷贝每个字段。
    • 基本类型:直接返回原始值。
  1. 递归拷贝:对于复杂数据结构(如切片中的切片,或 map 中嵌套 map),通过递归调用 deepCopy 来实现深度拷贝。

反射在实现动态类型处理时非常强大,但可能比直接手写深拷贝方法稍慢一些。因此,可以根据具体需求权衡是否使用。