slice
对于slice
来说,其实源码部分并不多,主要看一下make slice
、growslice
、slicecopy
和copy
编译阶段
make slice
语句跟map
一样,在compile
的typecheck
阶段,将MAKE
节点偷偷换成OMAKESLICE
节点
关于oppend
的代码实在/usr/local/go/src/cmd/compile/internal/gc/ssa.go
的func (s *state) append(n *Node, inplace bool)
,这个是编译时代码
关于slice
的代码在/usr/local/go/src/runtime/slice.go
,这部分是运行时代码
type slice struct
type slice struct {
array unsafe.Pointer
len int
cap int
}
array
就是slice
底层对应数组的内存地址,所有结构体的数据都是连续排列在这的,
len
当前的slice
数据大小
cap
当前的slice
数据容量
makelist
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
这里面其实len
和cap
都是默认传入进来的,实际make slice
只返回分配的数组内存地址,而len
和cap
还是结构体自己去记录的
growslice
func growslice(et *_type, old slice, cap int) slice {
// 这个 cap是即将要分配cap大小,例如
// s0 := make([]int, 2, 1)
// s0 = append(s0, 5, 6, 7, 8, 9)
// 那么这个时候生成调用代码growslice的参数cap就是 2+5=7
// 使用gdb 在 append那一行断点再step,就能看到数据
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
...
return slice{p, old.len, newcap}
}
上面主要是增长方式,记得三种情况就行:
- 如果发生
append
之后的cap
是超过原来的一倍,那么按超过的最大cap
作为新的cap
;否则进行以下逻辑 - 如果原来的
old cap
小于1024(注意这里不是内存,而是容量),那么扩容 1 倍(即原来的2倍);否则 - 扩容1.25倍(即原来的2.25倍)
slicecopy
func slicecopy(to, fm slice, width uintptr) int {
...
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
}
这里面的memmove
跟机器硬件指令有关(优化),都是使用汇编完成的;
有意思的是如果需要copy
的数据是 1 字节,是直接赋值这个字节
append
func (s *state) append(n *Node, inplace bool)
这个里面inplace
代表了两种使用append
的方式
If inplace is false, process as expression "append(s, e1, e2, e3)"
If inplace is true, process as statement "s = append(s, e1, e2, e3)"
不过inplace==false
在我现在的版本go version go1.12.5 linux/amd64
会编译错误
编译器希望我们处理一下append
的返回值
函数参数
func f(s1 []int) {
// s1[0] = 200
s1 = append(s1, 5, 6, 7, 8, 9)
fmt.Printf("f s1=%v\n", s1)
}
func main() {
s0 := make([]int, 1, 2)
s0[0] = 10
f(s0)
fmt.Printf("main s0=%v\n", s0)
}
// output
f s1=[10 5 6 7 8 9]
main s0=[10]
可以理解为,f(s0 []int)
其实调用是长这样的f(array *int, len int, cap int)
,如果在函数f
修改s1
切片中的某个值(例如上面的注释)是可以变动main
中s0
;
但是类似append
这种(如果cap
不够导致触发growslice
例外),可能会导致返回新的array
指针,就形成了局部变量,不会影响main
中的s0
数组
函数内部的数组(函数外部会保存在数据段),类似只有切片结构的array
字段,只有一个指针而已(指向一块分配好的内存),所以不能改变大小
// main.go
1 package main
2
3 import (
4 "fmt"
5 )
6
7 func main() {
8 array := [3]int{1, 2, 3}
9 slice := array[:2]
10 fmt.Printf("main s0=%v %v\n", array, slice)
11 }
go build -gcflags="all=-N -l" -o main main.go
gdb main
断点下在 第 10 行,
(gdb) info locals
&array = 0xc000016540
slice = {array = 0xc000016540, len = 2, cap = 3}