今天在看《go in action》的时候,看到切片这一章对于切片分配有点不太理解,做了几个实验理解了,感觉很有意义,记录下来。

首先我们要说一点,在golang中大部分都是值拷贝,但是slice并不是,而是基于长度进行的选择。 我们先做最简单的例子

1
2
3
4
5
6
7
func TestAppend(t *testing.T) {
	s1 := []int{1, 2, 3, 4, 5}
	s2 := s1[0:3]
	s2 = append(s2, 30)
	fmt.Println(s1, len(s1), cap(s1))
	fmt.Println(s2, len(s2), cap(s2))
}

可以看到输出结果是

1
2
[1 2 3 30 5] 5 5
[2 3 30] 3 4

很明显,对s2处理之后也修改了s1中的值,但是由于s2的容量相当于从0开始占据了s1的容量,所以总容量是4 我们可以很大胆地猜测,如果我们直接截取S1后往里面添加两个值,那么他自身的后两个值一定会被覆盖,我们做个实验:

1
2
3
4
5
6
7
func TestAppend(t *testing.T) {
	s1 := []int{1, 2, 3, 4, 5}
	s2 := s1[1:3]
	s2 = append(s2, 30, 40)
	fmt.Println(s1, len(s1), cap(s1))
	fmt.Println(s2, len(s2), cap(s2))
}

输出结果是:

1
2
[1 2 3 30 40] 5 5
[2 3 30 40] 4 4

和我们猜测的结果是一致的。如果我们添加的数据超出了s1的容量呢?

1
2
3
4
5
6
7
func TestAppend(t *testing.T) {
	s1 := []int{1, 2, 3, 4, 5}
	s2 := s1[1:3]
	s2 = append(s2, 30, 40, 50)
	fmt.Println(s1, len(s1), cap(s1))
	fmt.Println(s2, len(s2), cap(s2))
}

程序的输出结果是

1
2
[1 2 3 4 5] 5 5
[2 3 30 40 50] 5 8

可以发现,程序没有覆盖原来s1的数据,而是直接开辟了一段新的空间,将原来的值赋值进去,创建了大小为8的空间。为什么会是8呢? 我们不妨做个实验:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func TestAppend2(t *testing.T) {
	var tmp []int
	s2 := []int{1, 2, 3, 4}
	fmt.Print(cap(s2), ",")
	tmp = make([]int, 2)
	s2 = append(s2, tmp...)
	fmt.Print(cap(s2), ",")
	tmp = make([]int, 4)
	s2 = append(s2, tmp...)
	fmt.Print(cap(s2), ",")
	tmp = make([]int, 6)
	s2 = append(s2, tmp...)
	fmt.Print(cap(s2), ",")
	tmp = make([]int, 7)
	s2 = append(s2, tmp...)
	fmt.Println(cap(s2), ",")
}

大家可以猜测一下程序的输出结果

1
4,8,16,16,32

发现没有,每次添加数据之后,都是相对本slice的容量进行两倍放大,所以可以理解成一个慢性增的过程吧! 重新回到slice值传递,如果我们每次增加一个值,那么会覆盖吗? 我们再做个实验:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func TestAppend(t *testing.T) {
	s1 := []int{1, 2, 3, 4, 5}
	s2 := s1[1:3]
	s2 = append(s2, 30)
	fmt.Println(s1, len(s1), cap(s1))
	fmt.Println(s2, len(s2), cap(s2))
	s2 = append(s2, 40)
	fmt.Println(s1, len(s1), cap(s1))
	fmt.Println(s2, len(s2), cap(s2))
	s2 = append(s2, 50)
	fmt.Println(s1, len(s1), cap(s1))
	fmt.Println(s2, len(s2), cap(s2))
}

输出结果如下

1
2
3
4
5
6
[1 2 3 30 5] 5 5
[2 3 30] 3 4
[1 2 3 30 40] 5 5
[2 3 30 40] 4 4
[1 2 3 30 40] 5 5
[2 3 30 40 50] 5 8

也就是逐步覆盖的,直到超出容量为本slice分配新的空间

所以我们可以得到结论了,初始从slice进行截取操作的时候,是类似于指针传递,指向了原来的数组下标的位置,一旦超出本slice的容量,就会相对本slice大小开辟双倍的slice大小,进行存储。 我们在以后程序涉及的时候,需要考虑到一个问题了,如果希望对slice进行值拷贝,就不要直接使用截取操作了,而是使用append或者copy进行值传递才能保证数据安全。