Go切片小结 | Go主题月

801 阅读8分钟

之前我们介绍了数组,相信大家都感觉到了Go数组的不方便性,Go中数组一被定义,大小就无法改变,为此,就有了切片的诞生。

接下来我们介绍Go中的一个难点:切片(slice),体会到Go这种偏底层的代码语言的魅力。

1. 声明切片

var name []T
  • name: 切片名

  • T:切片中元素类型

可以将切片简单理解为:一个没有定义大小的数组。

slice类型变量不保存值,只保存(长度len ,容量cap,指向底层数组的指针point),故slice对象在初始化前没有分配内存,所以就根本没有默认值 = nil

package  main
import "fmt"
//切片
 
func main(){
  //切片在没有初始化情况下
	var slice []int
	fmt.Println(slice == nil)
}

输出结果:

true 

2. 初始化切片

(1) 直接初始化切片

var s1 = []int{1,2} //定义一个存放int类型元素的切片

(2) 从数组中获得切片数据

//由数组得到切片
 a1 := [...]int{1,2,3,4,5,6}
 a2 := a1[0:4] //得到[1,2,3,4],和python的切片一样

3. 通过make函数创建切片

//make( []T,  size,  cap )
//创建一个长度为6,容量为10的整形切片
a1 := make([]int, 6, 10) 
  • T: 切片中元素类型
  • size : 切片访问元素的个数(即长度),通过len(a1)获得
  • cap : 切片允许增长到的元素个数(即容量),通过cap(a1)获得
  • cap值可以省略,当cap省略时,默认与size值相等,即 a2 := make([]int ,5) ,则size = 5,cap = 5。

4. 切片中长度与容量的区别:

  1. 切片指向一个底层的数组,这个底层数组为切片存值

  2. 切片的长度就是它元素的个数

  3. 切片的容量是从底层数组从切片的第一元素到最后一个元素的数量

    截屏2021-04-14 下午7.14.47

    截屏2021-04-14 下午7.15.15

package  main
import "fmt"
//切片
 
func main(){
	//切片中长度与容量的区别
	slice := [...]int{1,2,3,4,5,6}
  fmt.Printf("slice:%v len(slice):%d cap(slice):%d\n",slice,len(slice),cap(slice))
  
	newslice := slice[0:2]// [1,2]
	fmt.Printf("newslice:%v len(newslice):%d cap(newslice):%d\n",newslice,len(newslice),cap(newslice))
	
	newslice2 := slice[2:5] // [3,4,5]
	fmt.Printf("newslice2:%v len(newslice2):%d cap(newslice2):%d",newslice2,len(newslice2),cap(newslice2))
}

输出结果:

slice:[1 2 3 4 5 6] len(slice):6 cap(slice):6
newslice:[1 2] len(newslice):2 cap(newslice):6
newslice2:[3 4 5] len(newslice2):3 cap(newslice2):4

5. 切片的本质:

  1. 切片就是一个框,框住了一块连续的内存

  2. 切片属于引用类型,真正的数据都是保存在底层的数组中(切片不保存值,值都是保存在底层数组中)。

package  main
import "fmt"
//切片
 
func main(){
	//切片中长度与容量的区别
	slice := [...]int{1,2,3,4,5,6}
	fmt.Printf("slice:%v len(slice):%d cap(slice):%d\n",slice,len(slice),cap(slice))
	newslice := slice[0:4]// [1,2,3,4]
	fmt.Printf("newslice:%v len(newslice):%d cap(newslice):%d\n",newslice,len(newslice),cap(newslice))
	
	newslice2 := slice[2:5] // [3,4,5]
	fmt.Printf("newslice2:%v len(newslice2):%d cap(newslice2):%d",newslice2,len(newslice2),cap(newslice2))
	
	//改变slice中值,newslice与newslice2中对应位置值跟着改变
	slice[3] = 100
	fmt.Println("\n改变后:")
	fmt.Printf("slice:%v len(slice):%d cap(slice):%d\n",slice,len(slice),cap(slice))
	fmt.Printf("newslice:%v len(newslice):%d cap(newslice):%d\n",newslice,len(newslice),cap(newslice))
	fmt.Printf("newslice2:%v len(newslice2):%d cap(newslice2):%d",newslice2,len(newslice2),cap(newslice2))

	//改变newslice值,slice与newslice2中相应位置值也跟着改变,两个切片共享同一底层数组
	newslice[2] = 99
	fmt.Println("\n再次改变后:")
	fmt.Printf("slice:%v len(slice):%d cap(slice):%d\n",slice,len(slice),cap(slice))
	fmt.Printf("newslice:%v len(newslice):%d cap(newslice):%d\n",newslice,len(newslice),cap(newslice))
	fmt.Printf("newslice2:%v len(newslice2):%d cap(newslice2):%d",newslice2,len(newslice2),cap(newslice2))
}

输出结果:

slice:[1 2 3 4 5 6] len(slice):6 cap(slice):6
newslice:[1 2 3 4] len(newslice):4 cap(newslice):6
newslice2:[3 4 5] len(newslice2):3 cap(newslice2):4
改变后:
slice:[1 2 3 100 5 6] len(slice):6 cap(slice):6
newslice:[1 2 3 100] len(newslice):4 cap(newslice):6
newslice2:[3 100 5] len(newslice2):3 cap(newslice2):4
再次改变后:
slice:[1 2 99 100 5 6] len(slice):6 cap(slice):6
newslice:[1 2 99 100] len(newslice):4 cap(newslice):6
newslice2:[99 100 5] len(newslice2):3 cap(newslice2):4

6. append()为切片添加元素

(1) append()追加一个元素

  1. 调用append函数必须用原来的切片变量接收返回值
  2. append追加元素,原来的底层数组装不下的时候,Go就会创建新的底层数组来保存这个切片
package  main
import "fmt"
//切片进阶操作
 
func main(){
	//append()为切片追加元素
	s1 := []string {"火鸡面","辛拉面","汤达人"}
	fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d\n",s1,len(s1),cap(s1))
	
	//调用append函数必须用原来的切片变量接收返回值
	s1 = append(s1,"小当家") //append追加元素,原来的底层数组装不下的时候,Go就会创建新的底层数组来保存这个切片
	fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d\n",s1,len(s1),cap(s1))
}

输出结果:

s1=[火鸡面 辛拉面 汤达人] len(s1)=3 cap(s1)=3
s1=[火鸡面 辛拉面 汤达人 小当家] len(s1)=4 cap(s1)=6

(2)append()追加一个切片

package  main
import "fmt"
//切片进阶操作
 
func main(){
	//append()为切片追加元素
	s1 := []string {"火鸡面","辛拉面","汤达人"}
	fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d\n",s1,len(s1),cap(s1))
	
	//调用append函数必须用原来的切片变量接收返回值
	s1 = append(s1,"小当家") //append动态追加元素,原来的底层数组容纳不下足够多的元素时,切片就会开始扩容,Go底层数组就会把底层数组换一个
	fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d\n",s1,len(s1),cap(s1))

	//调用append添加一个切片
	s2 := []string{"脆司令","圣斗士"}
	s1 = append(s1,s2...)//...表示拆开切片
	fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d",s1,len(s1),cap(s1))
}

输出结果:

s1=[火鸡面 辛拉面 汤达人] len(s1)=3 cap(s1)=3
s1=[火鸡面 辛拉面 汤达人 小当家] len(s1)=4 cap(s1)=6
s1=[火鸡面 辛拉面 汤达人 小当家 脆司令 圣斗士] len(s1)=6 cap(s1)=6% 

7. 切片扩容策略

$GOROOT/src/runtime/slice.go源码:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
	newcap = cap
} else {
	if old.len < 1024 {
		newcap = doublecap
	} else {
		// Check 0 < newcap to detect overflow
		// and prevent an infinite loop.
		for 0 < newcap && newcap < cap {
			newcap += newcap / 4
		}
		// Set newcap to the requested cap when
		// the newcap calculation overflowed.
		if newcap <= 0 {
			newcap = cap
		}
	}
}

从以上代码可以看出:

  1. 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。

  2. 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),

  3. 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)

  4. 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的扩容策略就不一样。

8. Copy()函数

在5中,我们知道slice中值的改变,会导致原底层数组中相应位置值的改变。

而Go中的copy()函数可以很快的将一个切片的值复制到另一个切片空间中,改变原切片的值不会影响新切片。

copy复制属于值复制,而等号复制为指针复制。

copy格式:

将srcSlice中元素复制到destSlice中

copy(destSlice,srcSlice []T) 
package main

import "fmt"
//copy
func main(){
	n1 := []int{1,2,3}
	//n2 := n1
	fmt.Printf("n1:%v,len(n1):%d,cap(n1):%d\n",n1,len(n1),cap(n1))
	//fmt.Println(n2)
	
	n3 := make([]int,6,6)
	fmt.Printf("n3:%v,len(n3):%d,cap(n3):%d\n",n3,len(n3),cap(n3))
	//源切片长度为6,目标切片长度为3,只会复制前3个
	copy(n1,n3)
	fmt.Printf("n1:%v,len(n1):%d,cap(n1):%d\n",n1,len(n1),cap(n1))
	fmt.Printf("n3:%v,len(n3):%d,cap(n3):%d\n",n3,len(n3),cap(n3))

	n4 := []int{5,6,7}
	//将源切片中3个值复制到目标切片前3个索引中
	n5 := copy(n3,n4) //n5输出索引到达的位置,为3
	fmt.Printf("n3:%v,len(n3):%d,cap(n3):%d\n",n3,len(n3),cap(n3))
	//copy复制不会改变源切片的值,也就是n4没有改变
	fmt.Printf("n4:%v,len(n4):%d,cap(n4):%d\n",n4,len(n4),cap(n4))
	fmt.Println(n5)
}

输出结果:

n1:[1 2 3],len(n1):3,cap(n1):3
n3:[0 0 0 0 0 0],len(n3):6,cap(n3):6
n1:[0 0 0],len(n1):3,cap(n1):3
n3:[0 0 0 0 0 0],len(n3):6,cap(n3):6
n3:[5 6 7 0 0 0],len(n3):6,cap(n3):6
n4:[5 6 7],len(n4):3,cap(n4):3
3

9. 从切片中删除元素

package main

import "fmt"
//delete
func main(){
	//目标删除索引为4的元素,也就是52
	n := []int{12,23,34,43,52,69,70}
	//将切片从索引处分割开,成两个切片,再用append连接起来
	n = append(n[:4],n[5:]...)
	fmt.Println(n)
}