什么是切片
slice
翻译成中文就是切片
,它和数组(array)
很类似,可以用下标的方式进行访问,如果越界,就会产生 panic。
数组在使用上有两点不足:
-
固定的元素个数,
-
传值机制下导致的开销较大。
于是 Go 设计者们引入了另外一种同构复合类型:切片(slice),来弥补数组的这两处不足。
总结,数组与slice的区别
-
数组类型的值(以下简称数组)的长度是固定的
-
切片类型的值(以下简称切片)是可变长的。
切片的创建与初始化
直接声明
// names为切片名称,[]string为切片类型
var names []string
这种方式创建出来的 slice 其实是一个 nil slice
。它的长度和容量都为 0。和nil
比较的结果为true
Nil slice
可以直接调用 append 函数来获得底层数组的扩容而不会panic。这是因为go的底层会帮助我们调用 mallocgc
来向 Go 的内存管理器申请到一块内存,然后再赋给原来的nil slice
,然后摇身一变,成为 “真正” 的 slice
了。
但是,直接对nil slice 赋值还是会panic
var sl []int
sl[0] = 13 // panic
sl = append(sl, 13) // ok
注意:
-
nil slice不等于empty slice
-
对nil slice 可以进行append操作
-
对nil slice 不可以赋值操作,会panic
通过make函数创建切片
// 创建一个整型切片,其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)
// len: the number of elements in v; if v is nil, len(v) is zero.
len(slice) = 3
如果没有在 make 中指定 cap 参数,那么底层数组长度 cap 就等于 len,比如:
// 创建一个整型切片,其长度和容量都是 5 个元素
slice := make([]int, 5)
通过字面量创建切片
// 创建字符串切片 其长度和容量都是3个元素
myStr := []string{"Jack", "Mark", "Nick"}
// 创建一个整型切片 其长度和容量都是4个元素
myNum := []int{10, 20, 30, 40}
// 设定切片长度为0,注意和nil slice的区别
myNum := []int{}
切片的常用操作
动态扩容
“动态扩容”指的就是,当我们通过 append 操作向切片追加数据的时候,如果这时切片的 len 值和 cap 值是相等的,也就是说切片底层数组已经没有空闲空间再来存储追加的值了,Go 运行时就会对这个切片做扩容操作,来保证切片始终能存储下追加的新值。
func append(slice []Type, elems ...Type) []Type
append
函数的参数长度可变,因此可以追加多个值到 slice
中,还可以用 ...
传入 slice,直接追加一个切片。
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
执行 append()
操作,如果slice的容量仍然能够容纳新的元素,则slice不会发生扩容。否则底层数组就会扩容
-
在切片的容量小于
1000
个元素时,总是会成倍地增加容量。 -
一旦元素个数超过
1000
,容量的增长因子会设为1.25
,也就是会每次增加25%
的容量
需要注意的是,slice在扩容后会新建新的slice,仍然指向原底层数组。因此我们通常需要使用原slice来接收append的结果
在 golang 的文档中关于
append
方法有这样一句话,“It is therefore necessary to store the result of append, often in the variable holding the slice itself”,即执行append
操作,一般会将append的结果赋值给原来的 slice。
遍历切片
Go的SDK中提供了两种遍历slice的方式
Index for loop
nums := []int{1, 2, 3}
for i := 0; i < len(nums); i++ {
fmt.Println(nums[i])
}
// ouput:
// 1
// 2
// 3
for range 遍历
使用 for…range
对切片中的元素进行遍历:
num1 := []int{1, 2, 3}
for index, value := range num1 {
fmt.Printf("index: %d value: %d\\n", index, value)
}
// output:
// index: 0 value: 1
// index: 1 value: 2
// index: 2 value: 3
当迭代切片时,关键字 range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本。
不过需要特别注意的是, for range 属于go sdk中的语法糖,其底层源码如下:
// len_temp := len(range)
// range_temp := range
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = range_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
特别注意:
-
For range操作会首先copy一份新的slice,其pointer 指向原底层数组
-
value值在遍历过程中,始终是一个变量。
求切片长度
len(xxx)
查看源码注解:
Slice, or map: the number of elements in v; if v is nil, len(v) is zero.
slice源码
Slice底层表示如下
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
slice
共有三个属性:
-
指针
: 指向底层数组; -
长度
: 表示切片可用元素的个数。使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度; -
容量
: 底层数组的元素个数
注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
Slice 注意点
empty slice和nil slice
// nil slice
var s1 []int
// empty slice
var s2 = []int{}
slice作为参数传递
如下代码
func main() {
nums := make([]string, 2, 6)
nums[0] = "a"
nums[1] = "b"
// 0x140000a6108
fmt.Println(fmt.Sprintf("%p", &nums))
modify(nums)
// [c b]
fmt.Println(nums)
}
func modify(nums[]string) {
// 0x140000a6120
fmt.Println(fmt.Sprintf("%p", &nums)) // 这里的nums是copy的main函数中的nums 因此指针不一样
nums[0] = "c"
nums = append(nums, "d")
}
输出结果说明:
-
Slice 作为参数传递,其传递的是slice引用的副本。
-
modifySlice函数中,nums[0] = "c" 会修改slice底层指向的数组,因此也会影响到main函数中的slice
-
Append 函数会首先判断是否需要扩容,如果需要则会新建slice,并且指向原底层数组
If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated.
for range中并发执行
下面的例子中预期在for range 遍历中输出[1,2,3,4]。 但是遗憾的是输出结果为[4,4,4,4]。原因正如3.2.1中所述,for range遍历过程只会存在一个num 变量。每次循环都共享同一个num变量。因此即使第一个协程拿到的num值为1,但是真正执行fmt.println 时 num的值已经被更新为4了。因此所有协程打印的num值都为4。
func main() {
nums := []int64{1, 2, 3, 4}
for _, num := range nums {
go func() {
fmt.Println(num)
}()
}
// output: [4,4,4,4]
time.Sleep(time.Second)
}