切片 是Go中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。它非常灵活,支持自动扩容。切片的底层一段连续的内存。
切片的内部实现
切片是一个有三个字段的数据结构,分别为地址
、长度
和容量
,它对底层的数组(内部是通过数组保存数据的)进行了抽象,并提供相关的操作方法。
地址:指向底层数组的指针。
长度:切片可以访问的元素的个数,使用内置函数len(切片名)
可以获得。
容量:从切片地址开始到底层数组结尾的长度,使用内置函数cap(切片名)
可以获得。
切片的创建和初始化
在Golang中可以通过多种方式创建和初始化切片。可以根据切片所需的容量来决定如何创建切片。
切片的声明
切片的声明格式如下:
var 切片名 []元素类型
例如,声明一个地址为nil的整型切片:
var myNum []int // 例:声明一个地址为nil的整型切片
单纯声明后的切片并没有分配内存空间,因此地址指向nil,可以称之为nil切片。nil切片的长度和容量都为0,数据结构状态如下:
在Golang中,nil切片很常见,可以使用很多标准库和内置函数。在需要描述一个不存在的切片时,nil切片会很好用。比如,函数要求返回一个切片但是发生异常的时候。
通过make()函数创建切片
使用Golang内置的make()函数,动态创建一个切片,格式如下:
make([]类型, 长度, 容量)
a := make([]int, 3, 5) // 定义一个长度为3,容量为5,元素为int类型的切片
fmt.Println(a) // [0 0 0] 切片可访问的元素数为3
fmt.Println(len(a)) // 3
fmt.Println(cap(a)) // 5
fmt.Printf("%#v \n", a) // []int{0, 0, 0}
示例代码中a
的内部存储空间已经分配了5个,但实际只是用了3个。容量并不会影响当前元素的个数。
当要创建的切片长度=容量
时,可以使用以下格式进行创建:
make([]类型, 长度) // 创建长度=容量的切片
b := make([]int, 3) // 定义一个长度为3,容量为3,元素为int类型的切片
fmt.Println(a) // [0 0 0]
fmt.Println(len(a)) // 3
fmt.Println(cap(a)) // 3
fmt.Printf("%#v \n", a) // []int{0, 0, 0}
创建切片时,长度和容量是可以为0的,此时地址指针不为nil,称为空切片。下图描述了空切片的状态。
通过字面量创建切片
另一种常用的创建切片的方法是使用切片字面量,这种方法和创建数组类似,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定:
// 创建字符串切片
// 其长度和容量都是3个元素
myStr := []string{"Jack", "Mark", "Nick"}
fmt.Printf("myStr = %#v,长度=%d,容量=%d \n", myStr, len(myStr), cap(myStr))
// output: myStr = []string{"Jack", "Mark", "Nick"},长度=3,容量=3
// 创建一个整型切片
// 其长度和容量都是4个元素
myNum := []int{10, 20, 30, 40}
fmt.Printf("%#v,长度=%d,容量=%d \n", myNum, len(myNum), cap(myNum))
// output: []int{10, 20, 30, 40},长度=4,容量=4
当使用切片字面量创建切片时,还可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。下面的语法展示了如何使用索引方式创建长度和容量都是100个元素的切片:
// 创建字符串切片
// 使用空字符串初始化第 100 个元素
myStr := []string{99: ""}
通过切片/数组创建切片
切片的本质是底层数组切出的一部分,因此可以通过字符串/数组/指向数组或切片的指针构造新切片。
它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外,还指定容量的完整的形式。
创建新切片的语法如下:
slice[i:j] (array[i:j])
slice[i:j:k] (array[i:j:k])
i: 表示从 slice/array 的第几个元素开始切
j: 控制切片的长度(j-i),一般不包含索引为j的元素。
k: 控制切片的容量(k-i),如果没有给定 k,则表示切到底层数组的最尾部。
下面是几种常见的简写形式:
slice[i:] // 从 i 切到最尾部
slice[:j] // 从最开头(0)切到 j(不包含j)
slice[:] // 从头切到尾,等价于复制整个slice
让我们通过下面的例子来理解通过切片创建新的切片的本质:
// 通过字面量创建一个整型切片
// 其长度和容量都是 5 个元素
myNum := []int{10, 20, 30, 40, 50}
// 创建一个基于myNum 的新切片
// i = 1, j = 3,k未指定,切到最尾部
// 长度 = j-i = 2 个元素,容量为 4 个元素
newNum := myNum[1:3]
fmt.Printf("newNum=%v,长度=%d,容量=%d \n", newNum, len(newNum), cap(newNum))
// output: newNum=[20 30],长度=2,容量=4
执行上面的代码后,我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分:
注意:截取新切片时的原则是 左含右不含
。所以 newNum 是从 myNum 的 index=1 处开始截取,截取到 index=3 的前一个元素,也就是不包含 index=3 这个元素。所以,新的 newNum 是由 myNum 中的第2个元素、第3个元素组成,长度为 2,容量为 4。切片 myNum 能够看到底层数组全部 5 个元素的容量,而 newNum 能看到的底层数组的容量只有 4 个元素。newNum 无法访问到底层数组的第一个元素。所以,对 newNum 来说,那个元素就是不存在的。
对切片的操作
为切片添加元素(append())
Go语言的内置函数append()
可以为切片动态添加元素。一次可以添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
var s []int
// 添加一个元素
s = append(s, 1) // [1]
// 添加多个元素
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
// 添加另一个切片的所有元素
s = append(s, s2...) // [1 2 3 4 5 6 7]
注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。
切片扩容
相对于数组而言,使用切片的一个好处是:可以按需增加切片的容量。Golang 内置的 append() 函数会处理增加长度时的所有操作细节。要使用 append() 函数,需要一个被操作的切片和一个要追加的值,当 append() 函数返回时,会返回一个包含修改结果的新切片。函数 append() 总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量。
例如:
func main() {
//append()添加元素和切片扩容
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
}
}
输出结果:
[0] len:1 cap:1 ptr:0xc000014098
[0 1] len:2 cap:2 ptr:0xc0000140e0
[0 1 2] len:3 cap:4 ptr:0xc0000121e0
[0 1 2 3] len:4 cap:4 ptr:0xc0000121e0
[0 1 2 3 4] len:5 cap:8 ptr:0xc00000e340
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc00000e340
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc00000e340
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc00000e340
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc000018100
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc000018100
从上面的结果可以看出:
append()
函数将元素追加到切片的最后,并返回该切片。- 切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。
函数 append() 会智能地处理底层数组的容量增长。在切片的容量小于 1024 个元素时,总是会成倍地增加容量。一旦元素个数超过 1024,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量(随着语言的演化,这种增长算法可能会有所改变)。
想要了解切片详细的扩容策略,可以查看$GOROOT/src/runtime/slice.go
的源代码。
删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素(32)
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}
总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)
遍历切片
切片的遍历方式和数组是一致的,支持索引遍历
和for range遍历
。
func main() {
s := []int{1, 3, 5}
// 索引遍历
for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}
// for range遍历
for index, value := range s {
fmt.Println(index, value)
// value += 1 只能修改副本值,无法修改切片元素内容
s[index] += 1 //可以修改切片元素内容
}
}
需要注意的是,for range
创建了每个元素的副本,而不是直接返回对该元素的引用。要想获取每个元素的地址,可以使用切片变量和索引值。
修改切片元素
使用切片变量和索引值修改切片元素的内容。
切片的复制
Golang 内置的 copy() 函数可以将一个切片中的元素拷贝到另一个切片中,其函数声明为:
func copy(dst, src []Type) int
它表示把切片 src 中的元素拷贝到切片 dst 中,返回值为拷贝成功的元素个数。如果 src 比 dst 长,就截断;如果 src 比 dst 短,则只拷贝 src 那部分:
num1 := []int{10, 20, 30}
num2 := make([]int, 5)
count := copy(num2, num1)
fmt.Println(count)
fmt.Println(num2)
运行这段单面,输出的结果为:
3
[10 20 30 0 0]
3 表示拷贝成功的元素个数。