最近在进行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底层实现