前言:slice是Go非常重要的数据结构,类似于Java语言中的ArrayList,二者有诸多的相似性,而相似性的根源来自于他们都是通过数组来实现的。使用slice时有诸多需要理解的地方。
1. slice作为参数传递给函数,为什么还是会被修改。看例子
func main() { var s []int for i := 1; i <= 3; i++ { s = append(s, i) } reverse(s) fmt.Println(s) } func reverse(s []int) { s[0] = 100 }
输出结果:[100 2 3]
分析:这里s作为值传递给函数reverse,按理说不会被函数修改才是。我们知道slice视同数组来实现的,s指向数组的起始位置。传递参数时,函数将这个指针拷贝一份,这时拷贝的指针还是指向原来的数组的起始地址,当函数利用拷贝的指针对地址中的数据进行修改后。我们再用s去访问这块地址时,就是被修改以后的数据。slice底层是数组,拷贝的是指针,原来的指针和拷贝的指针指向同一个块内存块。
2. 既然函数能修改slice,那么增加一个元素会不会反应到函数外面的slice呢?看例子
func main() { var s []int for i := 1; i <= 3; i++ { s = append(s, i) } fmt.Printf("before reverse the length: %d slice: %v \n",len(s),s) reverse(s) fmt.Printf("after reverese the length: %d slice: %v \n",len(s),s) } func reverse(s []int) { s = append(s, 999) fmt.Printf("before reverese and after append the length: %d slice: %v \n",len(s),s) for i, j := 0, len(s)-1; i < j; i++ { j = len(s) - (i + 1) s[i], s[j] = s[j], s[i] } fmt.Printf("after reverese and after append the length: %d slice: %v \n",len(s),s) }
输出结果:
before reverse the length: 3 slice: [1 2 3]
before reverese and after append the length: 4 slice: [1 2 3 999]
after reverese and after append the length: 4 slice: [999 3 2 1]
after reverese the length: 3 slice: [999 3 2]
分析:首先,可以看到s的长度是3,在reverse中拷贝了一个指针,并利用append添加了一个元素,长度变为4,值是[1 2 3 999] ,最后经过反转,slice长度仍然是4,值是[999 3 2 1] 。我们在函数外面读到的slice长度却是3,值竟然是[999 3 2]。我们知道拷贝的数组指针和原来的数组指针都指向同一块区域,拷贝指针对数据的修改会影响到原来的slice,但是却不会影响原来slice的长度,他的长度仍然为3,所以他只能读取[999 3 2 1] 的前三个元素,即[999 3 2]。slice作为函数参数时,他的数据可能会改变,但是属性例如长度、容量(后面会说)却不会变。
继续修改这个例子
func main() { var s []int for i := 1; i <= 3; i++ { s = append(s, i) } fmt.Printf("before reverse the length: %d capacity:%d slice: %v \n",len(s),cap(s),s) reverse(s) fmt.Printf("before reverse the length: %d capacity:%d slice: %v \n",len(s),cap(s),s) } func reverse(s []int) { s = append(s, 999) s = append(s, 999) fmt.Printf("before reverse the length: %d capacity:%d slice: %v \n",len(s),cap(s),s) for i, j := 0, len(s)-1; i < j; i++ { j = len(s) - (i + 1) s[i], s[j] = s[j], s[i] } fmt.Printf("before reverse the length: %d capacity:%d slice: %v \n",len(s),cap(s),s) }
输出结果:
before reverse the length: 3 capacity:4 slice: [1 2 3]
before reverse the length: 5 capacity:8 slice: [1 2 3 999 999]
before reverse the length: 5 capacity:8 slice: [999 999 3 2 1]
before reverse the length: 3 capacity:4 slice: [1 2 3]
分析:我们是指加了一行代码,给slice多添加了一个999,并且打印了slice的capacity,原来的slice居然没有变化。从第一行结果可以知道,slice的长度是3,容量是4,只能再添加一个元素。而拷贝的指针也是指向这个区域,当我们第一次添加999时没有问题,因为这个时候长度是3,容量是4。而当我们再一次添加999时,容量不够,系统会自动扩充容量,每次扩容为原理的2倍,原来是4,现在是8。并将原来的数据拷贝过去。这个时候再进行数据的反转就不是在原来数组上操作了,故原来slice不变。slice容量不够时,go会对其重新分配内存进行扩容,这时候进行的修改不会影响原来的slice。
3. slice的使用问题
func main() { var s1 []int s2 := make([]int, 1, 3) s3 := make([]int, 0, 3) for i := 1; i < 4; i++ { s1 = append(s1, i) s2 = append(s2, i) s3 = append(s3, i) fmt.Printf("s1: %v length: %d capacity: %d \n", s1, len(s1), cap(s1)) fmt.Printf("s2: %v length: %d capacity: %d \n", s2, len(s2), cap(s2)) fmt.Printf("s3: %v length: %d capacity: %d \n", s3, len(s3), cap(s3)) fmt.Println() } }
输出结果:
s1: [1] length: 1 capacity: 1
s2: [0 1] length: 2 capacity: 3
s3: [1] length: 1 capacity: 3
s1: [1 2] length: 2 capacity: 2
s2: [0 1 2] length: 3 capacity: 3
s3: [1 2] length: 2 capacity: 3
s1: [1 2 3] length: 3 capacity: 4
s2: [0 1 2 3] length: 4 capacity: 6
s3: [1 2 3] length: 3 capacity: 3
分析:用了三种方式,第一种直接定义使用,第二种是利用make,但是指定初始长度为0,容量为3,第二种是利用make,但是指定初始长度为1,容量为3。对第一种不推荐使用,因为没有指定容量,当天添加数据时,需要频繁扩容。第二种定义初始长度为1,但是角标为0的就用不到了。推荐使用第三种用法,指定数组容量的大小,并且从角标为0开始使用。
测试指定合适的容量与没有指定容量时的效率
func main() { var s1 []int s2 := make([]int, 0, 100000) start := time.Now().Unix() for i := 1; i < 100000000; i++ { s1 = append(s1, i) } end := time.Now().Unix() fmt.Printf("not set capacity time:%d \n",end-start) start = time.Now().Unix() for i := 1; i < 100000000; i++ { s2 = append(s2, i) } end = time.Now().Unix() fmt.Printf(" set capacity time:%d \n",end-start) }
输出:
not set capacity time:2
set capacity time: 1
分析:我们可以看到指定合适的容量可以大大加快程序的运行效率,但是也不可盲目的指定容量,造成内存的浪费。
总结:
1. slice作为参数时,拷贝的是指针,在没有扩容的时候,仍然与原来的slice指向相同的内存,利用拷贝指针对内存数据的修改会影响原来的slice。但是长度和容量并不会改变。
2. 涉及到拷贝指针扩容时,拷贝指针和原来slice指向的内存不一样,拥有不同的内存块,操作彼此独立。
3. 用make(type,0,capacity)来创建slice。
参考:Why are slices sometimes altered when passed by value in Go?