Go 语言中的拷贝和传值
2月 25, 2021
同很多其他语言如 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 的拷贝和传参的一些“诡异”现象都不诡异,已经非常明确了。