因为在Golang中,数组的长度是固定的,一旦定义,在后续的操作中就不能更改长度,在某些实际使用场景中就不是那么的的方便,这个时候我们就可以使用切片(slice)类型。
1. slice基本定义
Golang 的 slice 是一个引用类型,内部结构包含 地址
、长度
和容量
,一般用于快速地操作一块数据集合。真正的数据保存在底层的数组中,
使用上来说 slice 是一个拥有相同类型元素的可变长度的序列,具有灵活、可自动扩容等优点。
本质上,slice 是一个指向 array 的指针,当拷贝 slice 给另一个变量时,两个引用会指向同一个 array,类似Python中的深拷贝,从任何一个变量里修改,其他变量包括底层数组都会跟着改变。
相关的操作如下所示:
// 定义一个切片
var s1 []int // 定义一个存放int类型元素的切片
var s2 []string // 定义一个存放string类型的切片
fmt.Println(s1, s2)
fmt.Println(s1 == nil) // nil 就是 null
fmt.Println(s2 == nil) // nil 就是 null
// 初始化
s1 = []int{1, 2, 3, 4}
s2 = []string{"北京", "上海"}
fmt.Println(s1, s2)
fmt.Println(s1 == nil) // nil 就是 null
fmt.Println(s2 == nil) // nil 就是 null
// 切片的长度和容量 len cap
fmt.Printf("len(s1):%d cap(s1):%d\n", len(s1), cap(s1))
fmt.Printf("len(s2):%d cap(s2):%d\n", len(s2), cap(s2))
// 由数组得到切片
a := [...]int{1, 3, 5, 7, 11, 13, 17, 19}
s3 := a[0:5]
s4 := a[1:4]
s5 := a[3:]
fmt.Println(s3)
fmt.Println(s4)
fmt.Println(s5)
// 切片的容量是指底层数组从切片开始的第一个元素到最后元素的数量
fmt.Printf("len(s3):%d cap(s3):%d\n", len(s3), cap(s3))
fmt.Printf("len(s4):%d cap(s4):%d\n", len(s4), cap(s4))
fmt.Printf("len(s5):%d cap(s5):%d\n", len(s5), cap(s5))
// 切片的拷贝是深拷贝
s6 := s3
s3[1] = 200
s6[0] = 100
fmt.Printf("\n修改后的a:%d\n", a)
fmt.Printf("修改后的s3:%d\n", s3)
fmt.Printf("修改后的s6:%d\n", s6)
/*slice是引用类型,本质上是一个指向array的指针,当拷贝slice给另一个变量时,两个引用会指向同一个array*/
// 切片之后再切片
s7 := s3[2:]
fmt.Println("\ns3:", s3)
fmt.Println("s7:", s7)
fmt.Printf("len(s3):%d cap(s3):%d\n", len(s3), cap(s3))
fmt.Printf("len(s7):%d cap(s7):%d\n", len(s7), cap(s7)) // 切割两次之后仍然是以最开始的数组作为底层数组
运行结果如下图,
用make初始化一个slice,make([]int, 10, 100)
会分配一个有 100 个整数的底层数组,然后用长度 10 和容量 100 创建 slice 结构指向数组的前 10 个元素。
切片之间不能直接比较,无法使用==
来判断两个切片的元素是否一致,只能通过==
来判断一个slice是否为nil
,一个值为nil
的slice没有相应的底层数组,长度和容量都是0,但是长度和容量都是0的slice并不一定等于nil
,例如:
var s1 []int //len(s1) = 0, cap(s1) = 0, s1 == nil
s2 := []int //len(s2) = 0, cap(s2) = 0, s2 != nil
s3 =make([]int, 0) //len(s3) = 0, cap(s3) = 0, s3 != nil
2. slice的相关操作
append
使用append()
函数可以向slice追加元素,追加一个元素则slice的长度加1,此时,扩容了的slice的底层数组发生了改变,指向了一个新的数组,所以在调用append时,必须使用原来的变量进行接收。例如:
var b = make([]int, 0, 2)
fmt.Println(b)
fmt.Printf("len(b): %d cap(b): %d\n\n", len(b), cap(b))
b = append(b, 1, 2, 3)
fmt.Println(b)
fmt.Printf("len(b): %d cap(b): %d\n\n", len(b), cap(b))
b = append(b, 4, 5)
fmt.Println(b)
fmt.Printf("len(b): %d cap(b): %d\n\n", len(b), cap(b))
b = append(b, 6, 7)
fmt.Println(b)
fmt.Printf("len(b): %d cap(b): %d\n", len(b), cap(b))
b = append(b, 8, 9, 10, 11, 12, 13)
fmt.Println(b)
fmt.Printf("len(b): %d cap(b): %d\n", len(b), cap(b))
运行结果:
此外,当新增的内容,超出原slice的容量,则slice会依照下面的策略进行扩容,原文来自
李文周的博客
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
此外,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int
和string
类型的处理方式就不一样。
使用append可以删除slice中指定索引的元素,例如:
// 使用append删除slice中的某个元素
a1 := [...]int{2, 3, 5, 7, 11, 13, 17}
s1 := a1[:]
// 删除s1中索引为1的元素3
s1 = append(s1[:1], s1[2:]...)
fmt.Println(s1)
fmt.Println(a1) // 底层的数组也会将那个值删掉,并往后补上最后的元素的值
运行结果如下,
由于append括号中的除第一元素外都是要插入的新内容,其类型应该与slice中的元素类型一致,所以先要将一个slice中的所有元素添加到另一个slice中,需要用...
将slice中的元素读取出来。例如:
a := []int{1, 2, 3}
b := []int{4, 5, 6}
a = append(a, b...) //a = [1 2 3 4 5 6]