Golang 复合数据类型:切片

切片(slice)

切片的底层是数组实现的,可以按需自动增长和缩小。切片是数组的引用,因此是引用类型,不支持直接比较,只能和nil比较。切片的动态增长是通过内建函数append()来实现的,这个函数可以快速且高效地增长切片,也可以通过切片再次切割,缩小每一个切片的大小。

  • 切片不存值,底层数组存值
  • 切片指向一个底层数组
  • 底层数组是占用一块连续的内存空间

创建数组切片

创建两个类型分别为 int 型和 string 型的切片,并初始化

func main(){
    var slice1 []int
    var slice2 []string
    fmt.Println(slice1,slice2)
    fmt.Println(slice1 == nil) //true,没有开辟内存空间
    fmt.Println(slice2 == nil) //true
    //初始化
    slice1 = []int{1,3,5}
    slice2 = []string{"小","马","锅"}
    fmt.Println(slice1,slice2)
    fmt.Println(slice1 == nil) //false,初始化已分配内存空间
    fmt.Println(slice2 == nil) //false
}

/*
[] []
true
true
[1 3 5] [小 马 锅]
false
false
*/

可以看到,未初始化时的切片为 [ ],即切片的零值为 [ ]。由于切片是引用类型,切片与切片之间不能直接比较,只能与nil比较。未初始化的 slice1 和 slice2 由于没有分配到内存空间,因此与nil比较的值为 true ,初始化的 slice1 和 slice2 已经分配了内存空间,因此与 nil 比较的值为 false (分配的内存空间不同)。

  • make()函数创建切片
func main(){
    //创建长度5,容量10的切片,未被初始化
    s1 := make([]int, 5,10)
    fmt.Println(s1)
    fmt.Printf("s1长度:%d s1容量:%d\n",len(s1),cap(s1))
    //创建长度0,容量10的切片,空切片
    s2 := make([]int, 0 , 10)
    fmt.Println(s2)
    fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
}

/*
[0 0 0 0 0]
s1长度:5 s1容量:10
[]
s2长度:0 s2容量:10
*/
  • 使用切片字面量创建切片
func main(){
    s1 := []int{1,2,3,5,4,20}
    fmt.Println(s1)
    fmt.Printf("s1长度:%d s1容量:%d\n",len(s1),cap(s1))
    s2 := []string{"字","面","量"}
    fmt.Println(s2)
    fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
}

/*
[1 2 3 5 4 20]
s1长度:6 s1容量:6
[字 面 量]
s2长度:3 s2容量:3
*/

nil 切片和空切片

有时候需要声明一个值为 nil 的切片,也叫空切片;nil 切片在底层数组中包含 0 个元素,也没有分配任何的存储空间。nil 切片还可以用来表示空集合。一个 nil 切片没有底层数组,它的长度和容量都是 0 以下是创建空切片的三种方式:声明未初始化的切片使用make函数使用切片字面量

func main(){
    //声明未初始化的切片是空切片,值为 nil
    var slice []int
    fmt.Println(slice)
    //使用make函数创建空的字符型切片
    s1 := make([]string, 0)
    fmt.Println(s1)
    //使用字面量创建空的布尔型切片
    s2 := []bool{}
    fmt.Println(s2)
}

/*
[]
[]
[]
*/

长度和容量都为 0 的切片不一定都是 nil 切片(空切片),判断一个切片是否为空,应该使用 len(slice) == 0,不能使用 slice == nil 判断。

切片的长度和容量

切片拥有自己的长度和容量,可以通过使用内建函数 len() 求长度,cap() 求容量。

func main(){
    var slice1 []int
    var slice2 []string
    //初始化
    slice1 = []int{1,3,5,9,65,3}
    slice2 = []string{"小","马","锅"}
    //长度和容量
    fmt.Printf("slice1长度:%d slice1容量:%d\n",len(slice1),cap(slice1))
    fmt.Printf("slice2长度:%d slice2容量:%d\n",len(slice2),cap(slice2))
}

/*
slice1长度:6 slice1容量:6
slice2长度:3 slice2容量:3
*/

由数组得到切片

func main(){
    array := [...]int{2,3,5,1,8,13,31,9}
    //切片是基于底层数组的切片
    s := array[0:4]  //从第0个到第4个的切片(不包含最后一个) -> [2 3 5 1]
    s1 := array[1:6] //从第1个到第6个的切片(不包含最后一个) ->[3 5 1 8 13]
    s2 := array[:]  //全切(包含所有元素) -> [2 3 5 1 8 13 31 9]
    s3 := array[:5] //从第0个到第5个(不包含最后一个) -> [2 3 5 1 8]
    s4 := array[4:] //从第4个到结束(包括最后一个) -> [8 13 31 9]
    fmt.Printf("array:%d\n",array)
    fmt.Printf("s:%d\ns1:%d\ns2:%d\ns3:%d\ns4:%d\n",s,s1,s2,s3,s4)
}

/*
array:[2 3 5 1 8 13 31 9]
s:[2 3 5 1]
s1:[3 5 1 8 13]
s2:[2 3 5 1 8 13 31 9]
s3:[2 3 5 1 8]
s4:[8 13 31 9]
*/

切片的长度和容量

func main(){
    array := [...]int{2,3,5,1,8,13,31,9}
//    s := array[0:4]  //从第0个到第4个的切片(不包含最后一个) -> [2 3 5 1]
    s1 := array[1:6] //从第1个到第6个的切片(不包含最后一个) ->[3 5 1 8 13]
    s2 := array[:]  //全切(包含所有元素) -> [2 3 5 1 8 13 31 9]
    s3 := array[:5] //从第0个到第5个(不包含最后一个) -> [2 3 5 1 8]
    s4 := array[3:6] //从第3个到第6个(不包含最后一个)-> [1 8 13]
    
    //切片长度和容量与数组的关系
    fmt.Printf("array:%d\n",array)
    fmt.Printf("array长度:%d array容量:%d\n",len(array),cap(array))
    fmt.Printf("s1长度:%d s1容量:%d\n",len(s1),cap(s1))
    fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
    fmt.Printf("s3长度:%d s3容量:%d\n",len(s3),cap(s3))
    fmt.Printf("s4长度:%d s4容量:%d\n",len(s4),cap(s4))
}

/*
array:[2 3 5 1 8 13 31 9]
array长度:8 array容量:8
s1长度:5 s1容量:7
s2长度:8 s2容量:8
s3长度:5 s3容量:8
s4长度:3 s4容量:5
*/
  • 切片是引用类型,实际就是指向底层数组的指针
  • 切片的长度就是它的元素个数
  • 切片容量就是底层数组从切片第一个元素到最后一个元素的长度

s4

由切片得到切片

func main(){
    array := [...]int{2,3,5,1,8,13,31,9}
//    s := array[0:4]  //从第0个到第4个的切片(不包含最后一个) -> [2 3 5 1]
    s1 := array[1:6] //从第1个到第6个的切片(不包含最后一个) ->[3 5 1 8 13]
    s2 := s1[1:3]    //从s1切片第1个到第3个元素(不包含最后一个) -> [5 1]
    //切片长度和容量与数组的关系
    fmt.Printf("array:%d\n",array)
    fmt.Printf("array长度:%d array容量:%d\n",len(array),cap(array))
    fmt.Printf("s1长度:%d s1容量:%d\n",len(s1),cap(s1))
    //由切片组成切片
    fmt.Printf("s2:%d\n",s2)
    fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
    //切片是引用类型,如果改变底层数组,切片也会改变
    array[3] = 8888
    fmt.Printf("array修改第3个元素:%d\n",array)
    fmt.Printf("s1修改:%d\ns2修改:%d\n",s1,s2)
}

/*
array:[2 3 5 1 8 13 31 9]
array长度:8 array容量:8
s1长度:5 s1容量:7
s2:[5 1]
s2长度:2 s2容量:6
array修改第3个元素:[2 3 5 8888 8 13 31 9]
s1修改:[3 5 8888 8 13]
s2修改:[5 8888]
*/

可以看到,切片 s2 是由切片 s1 从第1个元素切到第3个元素(不包括最后一个)而形成的切片,为 [5 1] 。因为切片是对底层数组的引用,又称引用类型;因此,改变底层数组的值,切片也会改变。但是这里可以分为两种情况:

例如,使用 [ ] 修改底层数组 array 索引值3的元素为 8888 ,又因为 8888 分别是切片 s1 和 s2 对应索引值 2 和 1 的值,因此切片会发生改变;如果修改底层数组 array 索引值0的元素为 8888,又因为 8888 不在切片 s1 和 s2 的范围内,所以切片不会受到底层数组的改变而影响。

  • 若改变底层数组的索引值对应的元素,不在对应切片内的元素,则切片不会发生改变(情况一)
//情况一 · 修改底层数组元素不在切片范围内
func main(){
    array := [...]int{2,3,5,1,8,13,31,9}
    s1 := array[1:6] //从第1个到第6个的切片(不包含最后一个) ->[3 5 1 8 13]
    s2 := s1[1:3]    //从s1切片第1个到第3个元素(不包含最后一个) -> [5 1]
    //由切片组成切片
    fmt.Printf("s2:%d\n",s2)
    fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
    //切片是引用类型,如果改变底层数组,切片也会改变
    array[0] = 8888
    fmt.Printf("array修改第0个元素:%d\n",array)
    fmt.Printf("s1:%d\ns2:%d\n",s1,s2)
}

/*
s2:[5 1]
s2长度:2 s2容量:6
array修改第0个元素:[8888 3 5 1 8 13 31 9]
s1:[3 5 1 8 13]
s2:[5 1]
*/
  • 若改变底层数组的索引值对应的元素,是在对应切片内的元素,则切片也会发生改变(情况二)
////情况二 · 修改底层数组元素在切片范围内
func main(){
    array := [...]int{2,3,5,1,8,13,31,9}
    s1 := array[1:6] //从第1个到第6个的切片(不包含最后一个) ->[3 5 1 8 13]
    s2 := s1[1:3]    //从s1切片第1个到第3个元素(不包含最后一个) -> [5 1]
    //由切片组成切片
    fmt.Printf("s2:%d\n",s2)
    fmt.Printf("s2长度:%d s2容量:%d\n",len(s2),cap(s2))
    //切片是引用类型,如果改变底层数组,切片也会改变
    //情况二
    array[3] = 8888
    fmt.Printf("array修改第3个元素:%d\n",array)
    fmt.Printf("s1:%d\ns2:%d\n",s1,s2)
}

/*
s2:[5 1]
s2长度:2 s2容量:6
array修改第3个元素:[2 3 5 8888 8 13 31 9]
s1:[3 5 8888 8 13]
s2:[5 8888]
*/

切片的赋值

func main() {
    s1 := []string{"red", "yellow", "blue", "green"}
    fmt.Printf("s1:%s\n",s1)
    //修改第0个元素的值
    s1[0] = "black"
    s2 := s1 //s2和s1都指向同一个底层数组
    fmt.Printf("s1:%s\ns2:%s\n",s1,s2)
}

/*
s1:[red yellow blue green]
s1:[black yellow blue green]
s2:[black yellow blue green]
*/

切片遍历

  • 索引遍历
  • range遍历,总是从切片的头部(索引值为0的元素)开始的
func main(){
    array := []int{1,2,3}
    //按索引遍历
    for i:=0; i<len(array) ; i++ {
        fmt.Println(array[i])
    }
    //range遍历
    for i,v := range array {
        fmt.Println(i,v)
    }
}

/*
1
2
3
0 1
1 2
2 3
*/

切片扩容

  • 使用append()函数进行切片扩容

    • 调用append函数必须用原来的切片变量接收返回值
    • 必须用变量接收函数返回值
    • 若底层数组不够,append函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值
    • 切片扩容会根据切片类型不同而做不同的处理,比如 int 和 string 类型的处理方式是不一样的
func main(){
    //原始切片
    slice := []int{1,2,3}
    fmt.Printf("slice:%d len(slice):%d cap(slice):%d\n",slice,len(slice),cap(slice))
    //第一次切片扩容,观察长度和容量
    s1 := append(slice,4,5)
    fmt.Printf("s1:%d len(s1):%d cap(s1):%d\n",s1,len(s1),cap(s1))
    //第二次切片扩容,观察长度和容量
    s1 = append(s1,6,7,8)
    fmt.Printf("s1:%d len(s1):%d cap(s1):%d\n",s1,len(s1),cap(s1))
    //第三次切片扩容,观察长度和容量
    s1 = append(s1,9,10,11,12,15)
    fmt.Printf("s1:%d len(s1):%d cap(s1):%d\n",s1,len(s1),cap(s1))
    //追加原始切片slice元素 ... 代表拆分切片内元素,比如[1][2][3]
    s1 = append(s1, slice...)
    fmt.Printf("s1:%d len(s1):%d cap(s1):%d\n",s1,len(s1),cap(s1))
}

/*
slice:[1 2 3] len(slice):3 cap(slice):3
s1:[1 2 3 4 5] len(s1):5 cap(s1):6
s1:[1 2 3 4 5 6 7 8] len(s1):8 cap(s1):12
s1:[1 2 3 4 5 6 7 8 9 10 11 12 15] len(s1):13 cap(s1):24
s1:[1 2 3 4 5 6 7 8 9 10 11 12 15 1 2 3] len(s1):16 cap(s1):24
*/

函数append()会智能地处理底层数组的容量。可以看出,在创建新的底层数组时,与原始底层数组相比较,新数组容量是原始数组容量的两倍。在切片容量小于 1024 个元素时,总是会成倍地增长容量。一旦元素超过 1024 个,容量的增长因子将会设置为 1.25 ,也就是每次会增加 25% 的容量。【注】:关于切片容量增长的源代码在 $GOROOT/src/runtime/slice.go

切片复制

使用copy()函数进行切片复制

func main(){
    a1 := []int{1,3,5}
    a2 := a1
    var a3 = make([]int,3,3) 
    copy(a3,a1)  //把切片a1复制给a3(副本)
    fmt.Println(a1,a2,a3)
    a1[0] = 100  //修改a1索引值0的元素
    fmt.Println(a1,a2,a3) //a2共用a1的底层数组会改变,副本a3不受影响
}

/*
[1 3 5] [1 3 5] [1 3 5]
[100 3 5] [100 3 5] [1 3 5]
*/

切片删除

Go语言中没有删除切片的专用方法,但是可以利用切片的特性进行删除操作。

func main(){
    array := [...]int{1,2,3,4,5,6,7}  //底层数组
    slice := array[:]  //全切,把底层数组转化成切片类型
    //删除索引值2的元素,即删除3
    fmt.Printf("原始array容量:%d 原始slice容量:%d\n",cap(array),cap(slice))
    //先把[1 2]和[4 5 6 7]切开,再拼接
    slice = append(slice[:2],slice[3:]...)
    fmt.Println(slice)
    fmt.Printf("修改slice容量:%d\n",cap(slice)) //切片不存值,底层数组存,删除切片元素等价于元素向左移,内存空间没被删,容量也就不变
}

/*
原始array容量:7 原始slice容量:7
[1 2 4 5 6 7]
修改slice容量:7
*/

切片不存值,底层数组存值,删除切片元素可以看作是切片元素范围移动,而数组本身所占用的内存空间没有被删,因此原始切片容量和删除切片后容量是一致的,都是7

func main(){
    array := []int{1,2,3,4,6,8,5,13,25}
    slice := array[:]
    fmt.Printf("原始数组array:%d\n",array)
    //删除索引值为4的元素6
    slice = append(slice[:4],slice[5:]...)
    fmt.Printf("修改切片slice:%d\n",slice)
    fmt.Printf("修改数组array:%d\n",array)
}

/*
原始数组array:[1 2 3 4 6 8 5 13 25]
修改切片slice:[1 2 3 4 8 5 13 25]
修改数组array:[1 2 3 4 8 5 13 25 25]
*/

这里原始数组和修改数组不同的原因是:当删除索引值为4的元素6时,就相当于把 slice [ 5 : ] 这部分的切片元素都追加到 slice [ : 4 ] 后面,此时索引值是4的切片元素6已经被删除,而底层数组本身的内存空间是不改变的,因此在使用 append() 函数追加以后,底层数组最后一位元素是原始数组的最后一位元素,即 25 。

练习(demo)

一道考察切片基础的面试题

func main(){
    //未初始化时的切片是零值
    var slice = make([]int,5,10)
    fmt.Println(slice) 
    //在int型切片零值的基础上追加元素
    for i:=0 ; i<10 ; i++ {
        slice = append(slice,i)
    }
    fmt.Println(slice)
}

/*
[0 0 0 0 0]
[0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]
*/

发表评论

电子邮件地址不会被公开。 必填项已用*标注