Go 语言中的拷贝和传值

Go 语言中的拷贝和传值

Feb 25, 2021
Go

同很多其他语言如 Python、JavaScript 一样,在 Go 中涉及到复制数据的场景也需要注意深拷贝和浅拷贝的问题。

深拷贝和浅拷贝 #

Go 的数据类型可以分为值类型和引用类型两种。值类型的变量和变量的数据都是存储在栈中,Int、Float、Bool、Array、Sturct、String 等都是值类型。引用类型的变量为一串地址,存储在栈中,变量的数据存储在地址所指向的堆空间中 Slice、Map、Channel、Interface、指针、函数等是引用类型。

Go 语言中值类型的数据默认是深拷贝。深拷贝指的是重新创造了一个完全新的对象,新对象和之前的对象不共享内存,而是新开辟一个新的内存空间,各自的修改也不会相互影响。引用类型的数据默认都是浅拷贝,浅拷贝指的是只拷贝指向对象的指针,拷贝之后新老对象的数据是同一块内存。

下面开始看实际的例子。

Slice 的拷贝 #

内置的 copy 函数可以进行 Slice 的深拷贝,但是需要注意使用时要声明新切片的长度:

func main() {
	s := []string{"a", "b", "c", "d", "e"}
	fmt.Println(s) //[a b c d e]
	var cs []string
	copy(cs, s)
	fmt.Println(cs) //[]
	var cs1 = make([]string, len(s))
	copy(cs1, s)
	fmt.Println(cs1) //[a b c d e]

	cs1[0] = "x"
	fmt.Println(s) //[a b c d e]

	s[0] = "z"
	fmt.Println(s)   //[z b c d e]
	fmt.Println(cs1) //[x b c d e]
}

使用 copy 复制的新对象,改变切片的值不会影响之前的切片,而如果是用等号 := 复制,则为引用复制,改变原切片的值会对新切片产生影响:

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	s1 := s
	s1[0] = 100
	fmt.Println(s)  //[100 3 5 7 11 13]
	fmt.Println(s1) //[100 3 5 7 11 13]
}

Map 拷贝 #

Map 也是不能直接进行拷贝的:

func main() {
	m := make(map[string]string)
	m["a"] = "b"
	m1 := m
	m1["a"] = "c"
	fmt.Println(m) //map[a:c]
	fmt.Println(m1) //map[a:c]
}

如果要对 Map 进行深拷贝,最朴实无华的办法就是新 make 一个 map 然后 for 循环赋值了:

func main() {
	m := make(map[string]int)
	m["Answer"] = 42
	m["Answer1"] = 48

	m1 := make(map[string]int, len(m))
	for k, v := range m {
		m1[k] = v
	}
	fmt.Printf("m:%p, m1:%p\n", &m, &m1) // m:0x1400000e028, m1:0x1400000e030
	m["Answer"] = 48
	fmt.Println(m1) //map[Answer:42 Answer1:48]
}

传值还是传引用? #

先说结论,Go 的函数调用都是先拷贝值(这个值可以是值的值,也可以是引用的值),然后传值。有个技巧,引用类型的数据,在 Go 里面都是靠 make 来创建的,make 后返回的都是值的引用,并不是值本身,下面按照这个原则看例子。

Slice 传值 #

特别注意分别通过下面两种方式初始化的 Slice 在传值时有不同的结果:

func main() {
	x := [3]int{1, 2, 3}
	func(arr [3]int) {
		arr[0] = 4
		fmt.Println(arr) //[4 2 3]
	}(x)
	fmt.Println(x) //[1 2 3]

	s := make([]int, 3)
	s[0] = 1
	s[1] = 2
	s[2] = 3
	func(arr []int) {
		arr[0] = 4
		fmt.Println(arr) //[4 2 3]
	}(s)
	fmt.Println(s) //[4 2 3]
}

另外 Slice 需要注意的是如果传值后会通过 append 扩容,那么新的值就和原先函数的值没什么关系了,因为 append 返回了新的 Slice。

func main() {
	nums := []int{1,3,5,7}
	add(nums)
	fmt.Println(nums) // [1 3 5 7]
}

func add(s []int) {
	s = append(s, 0)
	s[0] = 2
}

Map 传值 #

func main() {
	m := make(map[string]int)
	m["Answer"] = 42
	func(ma map[string]int) {
		ma["Answer"] = 4
		fmt.Println(ma["Answer"]) // 4
	}(m)
	fmt.Println(m["Answer"]) // 4
}

Chan 传值 #

func main() {
	c := make(chan int, 2)
	c <- 1
	c <- 2
	func(ch chan int) {
		fmt.Println(<-ch) //1
	}(c)
	fmt.Println(<-c) //2
}

Struct 传值 #

Struct 本身就是值类型,在函数调用时也是值传递:

func main() {
	type S struct {
		a int
	}
	var s S
	s.a = 1
	func(st S) {
		st.a = 2
		fmt.Println(st.a) //2
	}(s)
	fmt.Println(s.a) //1
	func(st *S) {
		st.a = 3
		fmt.Println(st.a) //3
	}(&s)
	fmt.Println(s.a) //3
}

总结 #

只要明白了值类型和引用类型、深拷贝和浅拷贝的区别,在实际写代码中应该会稳很多,然后格外注意通过 make 初始化的数据在函数调用中的传参及后续使用,问题就不大了,总结下来发现其实 Go 的拷贝和传参的一些“诡异”现象都不诡异,已经非常明确了。

本文共 1186 字,上次修改于 Aug 10, 2022,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» Go 语言的 MPG 并发调度模型

» Go 语言的 Context 源码分析

» Go 语言中 Channel 的实现

» 了解下 Protobuf 相关概念

» Panic:assignment to entry in nil map