使用Go的slice数据结构应该注意的几点

前言: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?

推荐阅读:年终盘点!2017年超有价值的Golang文章

猜你喜欢

转载自blog.csdn.net/fengxiaozhuzhu/article/details/80930140