这是【深入浅出 go xx】系列的第一篇。在该系列中,会对 go 的一些基本特性,从定义到源码进行整理和记录。
希望不要跳票。
什么是 slice?
slice 是 Go 的一种基本的数据结构。它在数组之上做了一层封装,相当于动态数组。它持有对底层数组的引用。
因此: * 如果你将一个 slice 赋给另一个 slice,那么,这两个 slice 都会指向同一个数组。 * 如果一个函数的参数包含 slice,那么,对入参 slice 的元素的改动对于该函数的调用者是可见的。类似于传递一个指向底层数组的指针给函数。
每个 slice 都有两个属性:length 和 capacity。前者是 slice 的底层数组的元素个数,后者是底层数组的长度。length 会随着 slice 的元素改动在 capacity 的范围内发生改变。可以通过内置函数 cap
查看 slice 的 capacity,通过 len
查看 length。可以使用 append
方法将数据追加到 slice 之后。如果数据超过了 capacity,那么,会为 slice 重新分配空间,并返回最终的 slice。
注意:对
nil
slice 使用cap
和len
方法是合法的,此时会返回 0。
如何使用 slice
可以使用内置函数 make([]T, length, capacity)
创建一个新的已初始化的类型为 T
的 slice。例如:
1 | // 创建并初始化一个类型为 int,length 为 50,capacity 为 100 的 slice |
上面的代码等价于创建一个长度为 50 的数组,然后对其进行切片: 1
slc := ([100]int)[0:50]
1
2
3// 会创建一个 length 为 5,capacity 为 5 的 slice
// 注意:[] 里面不要写容量。否则创建的就是数组,而不是 slice 了。
slc := []int{1, 2, 3, 4, 5}
结构定义
可以在 /src/runtime/slice.go
中找到对 slice 的定义:
1 | type slice struct { |
创建切片
使用以下方法创建切片:
1 | func makeslice(et *_type, len, cap int) slice { |
还有一个针对大小和容量是 int64 的方法:
1 | func makeslice64(et *_type, len64, cap64 int64) slice { |
append()
golang 中有一个内置函数可以对 slice 进行追加。函数声明如下:
1 | func append(slice []Type, elems ...Type) []Type |
该函数会将 elems
追加到 slice
之后。如果 slice
有足够的 capacity,那么直接追加。否则,会分配一个新的底层数组。
有三种使用方式: 1
2
3slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
slice = append([]byte("hello "), "world"...)
1 | // growslice handles slice growth during append. |
拷贝
golang 提供了一个内置函数 copy
来进行 slice 拷贝。函数定义如下: 1
func copy(dst, src []Type) int
src
中的元素拷贝到目标 slice dst
中(有一个特殊场景:它还会将一个字符串中的字节拷贝到一个 bytes slice 中)。源和目标可能会发生重叠。 该函数返回拷贝的元素个数,即 min(len(src), len(dst)。
slice 的拷贝是通过以下函数实现的: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35func slicecopy(to, fm slice, width uintptr) int {
// 如果源 slice 或者目标 slice 的 length 为 0,则无需拷贝,直接返回
if fm.len == 0 || to.len == 0 {
return 0
}
// 记录源 slice 和目标 slice 之间最短的 length
n := fm.len
if to.len < n {
n = to.len
}
if width == 0 {
return n
}
if raceenabled { // 开启了竞争检测
callerpc := getcallerpc()
pc := funcPC(slicecopy)
racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
}
if msanenabled { // 开启了 the memory sanitizer
msanwrite(to.array, uintptr(n*int(width)))
msanread(fm.array, uintptr(n*int(width)))
}
size := uintptr(n) * width
if size == 1 { // common case worth about 2x to do here
// TODO: is this still worth it with new memmove impl?
*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
} else {
memmove(to.array, fm.array, size)
}
return n
}func slicestringcopy(to []byte, fm string) int
实现了上面提到的特殊场景。实现跟 slicecopy
相似,这里不再赘述。
slice 使用注意事项
当 slice 作为函数参数,并且在函数中进行修改时……
来源:GopherCon 2018: Jon Bodner - Go Says WAT
考虑以下例子: 1
2
3
4
5
6
7
8
9
10
11
12func grow(s []int) {
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
s = append(s, 4, 5, 6)
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
}
func main() {
s := []int{1, 2, 3}
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
grow(s)
fmt.Printf("value: %v, length: %d, capacity: %d, addr: %p\n", s, len(s), cap(s), s)
}1
2
3
4value: [1 2 3], length: 3, capacity: 3, addr: 0xc00000a460
value: [1 2 3], length: 3, capacity: 3, addr: 0xc00000a460
value: [1 2 3 4 5 6], length: 6, capacity: 6, addr: 0xc00000c330
value: [1 2 3], length: 3, capacity: 3, addr: 0xc00000a460grow()
之后得到的 s
是 [1 2 3]
,而不是 [1 2 3 4 5 6]
呢?
因为在 append
的时候,s
没有足够的容量。因此,会创建一个新的 slice。可以从上面的打印看到,在 append
调用前后,s
的容量和地址都发生了改变。因此,append
并不会对 main
函数中的 s
作出任何修改。
那么,如果我们给 s
设置了一个足够大的 capacity 呢?
1 | func grow(s []int) { |
运行输出: 1
2
3
4value: [1 2 3], length: 3, capacity: 10, addr: 0xc00007e0a0
value: [1 2 3], length: 3, capacity: 10, addr: 0xc00007e0a0
value: [1 2 3 4 5 6], length: 6, capacity: 10, addr: 0xc00007e0a0
value: [1 2 3], length: 3, capacity: 10, addr: 0xc00007e0a0grow()
之后得到的 s
还是 [1 2 3]
!
这是因为,上面提到了,一个 slice 是由 array
,len
和 cap
一起标识的,并且在作为入参时是按值传递的。因此,无论是在 main
函数,还是在 grow
函数,s
指向的都是同一个底层数组。但是,在 grow
函数中,s
的 len
值发生了改变,而在 main
中,s
的 len
值还是保持了原样。
另一方面,Go 使用 reflect
包的 Value.Pointer
来获取指针,此时,得到的是底层数组的指针,而不是 slice 的指针。因此,打印出来的指针是同一个。