最近在进行golang的学习,对于切片有一些需要注意的事项如下
切片的值传递
我们都知道,切片是对底层数组的一个引用,但是在go中,函数之间都是值传递(除非使用指针),因此对于以下的代码,你是不是觉得test的值并不会被改变?
package main
import "fmt"
func main() {
test := []int{1, 2, 3, 4, 5}
fmt.Printf("test: %v\n", test)
change(test)
fmt.Printf("test: %v\n", test)
}
func change(a []int) {
a[0] = 999
}
但实际上,输出的结果为
test: [1 2 3 4 5]
test: [999 2 3 4 5]
切片的值发生了改变,这是为什么呢? 我们在主函数以及change函数中分别打印一下地址看看
func main() {
test := []int{1, 2, 3, 4, 5}
fmt.Println("the address of test:",&test)
change(test)
}
func change(a []int) {
fmt.Println("the address of a:",&a)
}
the address of test: 0xc000059f40
the address of a: 0xc000059f58
发现,在主函数和change函数中,两个切片的地址确实不一样,那为什么原切片的值发生了改变呢?我们再来打印一下具体元素的地址。
func main() {
test := []int{1, 2, 3, 4, 5}
println("test中的元素地址:", &test[0])
change(test)
}
func change(a []int) {
println("a中的元素地址:", &a[0])
println(a)
}
test中的第一个元素的地址: 0xc000059f48
a中的第一个元素的地址: 0xc000059f48
发现,其实第一个元素的地址是相同,其实,每个元素的地址也都是相同的。因此,可以推断出,虽然函数中对test切片进行了值传递,但是传递过后还是指向了同样的一块内存空间,故可以直接对切片中的元素进行修改。
探索1
基于上述结论,让我们来猜想一下接下来这行代码是什么样的结果?
func main() {
test := []int{1, 2, 3, 4, 5}
fmt.Printf("test: %v\n", test)
append_test(test)
fmt.Printf("test: %v\n", test)
}
func append_test(a []int) {
a = append(a, 999)
}
根据上面的结论,第二次打印的test应该会增加一个元素,但是结果并不是,结果为
test: [1 2 3 4 5]
test: [1 2 3 4 5]
两次的结果是一样的,于是我们再打印一下地址来看看区别
func main() {
test := []int{1, 2, 3, 4, 5}
println("test的地址:", &test)
println("test中第一个元素的地址:", &test[0])
append_test(test)
}
func append_test(a []int) {
println("a的地址:", &a)
println("a中第一个元素的地址:", &a[0])
a = append(a, 999)
println("增加元素后,a的地址:", &a)
println("增加元素后,a中第一个元素的地址:", &a[0])
}
结果为
test的地址: 0xc000059f40
test中第一个元素的地址: 0xc000059f10
a的地址: 0xc000059f58
a中第一个元素的地址: 0xc000059f10
增加元素后,a的地址: 0xc000059f58
增加元素后,a中第一个元素的地址: 0xc000014280
可以发现,在调用append(a,999)前,a中元素的地址和test中元素的地址一致,但append之后,地址就不一致了,这是因为append的操作在面临length大于cap时,会另取一块内存空间,存放新的切片,也就是俗称的“切片搬家”,因此导致元素地址的变化。这不禁让我设想,倘若一开始就取一个大的cap,再调用函数append时,没有超出上限,会不会也对原切片产生影响(换句话说,避免这种搬家操作)? 来试试看
func main() {
test := make([]int, 5, 10)
for i := 0; i < 5; i++ {
test[i] = i+1
}
fmt.Printf("len(test): %v\n", len(test))
fmt.Printf("cap(test): %v\n", cap(test))
fmt.Printf("test: %v\n", test)
append_test(test)
fmt.Printf("test: %v\n", test)
}
func append_test(a []int) {
fmt.Printf("cap(a): %v\n", cap(a))
a =append(a, 999)
}
来看一下结果
len(test): 5
cap(test): 10
test: [1 2 3 4 5]
cap(a): 10
test: [1 2 3 4 5]
test切片的值仍然没有发生变化,于是我们打印一下test切片与函数中a切片的各个元素的地址
func main() {
test := make([]int, 5, 10)
for i := 0; i < 5; i++ {
test[i] = i + 1
}
append_test(test)
println("test切片中的各个元素地址:")
for i := 0; i < len(test); i++ {
println(&test[i])
}
}
func append_test(a []int) {
a = append(a, 999)
println("a切片中的各个元素地址:")
for i := 0; i < len(a); i++ {
println(&a[i])
}
}
结果如下
a切片中的各个元素地址:
0xc000059f20
0xc000059f28
0xc000059f30
0xc000059f38
0xc000059f40
0xc000059f48
test切片中的各个元素地址:
0xc000059f20
0xc000059f28
0xc000059f30
0xc000059f38
0xc000059f40
发现,在函数中,这两个切片仍然共用了一块切片,但是并不会对原切片进行修改。
结论
写了这么多,其实就搞清楚了一件事情,当在一个函数调用中对值传递过来的切片中的元素进行修改时,只有一种情况是有效的,那就是元素的地址不变的情况下,也就是在其他语言中的深拷贝和浅拷贝问题。
探索2
这又让我想起在有些时候对二维切片进行append操作时,所产生的浅拷贝和深拷贝问题,如下:
func main() {
a := [][]int{}
b := []int{1, 2, 3, 4, 5}
a = append(a, b)
fmt.Printf("a: %v\n", a)
b[0] = 999
fmt.Printf("a: %v\n", a)
}
结果如下:
a: [[1 2 3 4 5]]
a: [[999 2 3 4 5]]
a中加入的b切片,在b切片没有搬家时,对b切片的修改也会导致a切片的修改,因此,往往使用这样的语句来避免这种修改:
func main() {
a := [][]int{}
b := []int{1, 2, 3, 4, 5}
a = append(a, append([]int{}, b...))//建立一个新的切片
fmt.Printf("a: %v\n", a)
b[0] = 999
fmt.Printf("a: %v\n", a)
}
结果如下:
a: [[1 2 3 4 5]]
a: [[1 2 3 4 5]]
推荐阅读
https://halfrost.com/go_slice/
深入解析Go中Slice底层实现