Slice
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
Slice的特征
首先让我们定义一个slice
var s []int
这里我们定义slice的元素类型为int,其实slice的元素可以是任何类型[]T,
其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
在刚开始接触Go的时候并没有太多关注过slice如何管理它的容量(capability),只知道我可以向一个slice任意增减元素。
首先我们来看一个空切片的长度和容量
func main() {
var s []int
detail(s)
}
//打印slice长度和容量的函数
func detail(s []int) {
fmt.Printf("length: %d cap: %d \n", len(s), cap(s))
}
输出
length: 0 cap: 0
就是说,一个初始切片的长度和容量都是0。那么疑问就来了:既然容量为0那我能向slice中成功添加内容吗?
我们来试试:
func main() {
var s []int
s = append(s, 1)
detail(s)
}
输出
length: 1 cap: 1
可以看到slice自动地为我们扩容了。为了观察slice如何管理容量,接下来我们试着向slice中连续添加10个元素,并时刻关注它的长度和容量之间的关系。
func main() {
var s []int
for i := 0; i < 10; i++ {
s = append(s, 1)
detail(s)
}
}
输出
length: 1 cap: 1
length: 2 cap: 2
length: 3 cap: 4
length: 4 cap: 4
length: 5 cap: 8
length: 6 cap: 8
length: 7 cap: 8
length: 8 cap: 8
length: 9 cap: 16
length: 10 cap: 16
这次我们似乎发现了slice管理容量的规律:当元素超过自身容量时,将容量翻倍。
为了一探究竟:我们来稍微改造一下我们的函数:
我们定义一个值sCap用来观察切片s的容量,s的容量随着元素个数变化。当s的容量变化时,将它打印出来:
func main() {
var s []int
var sCap int
for i := 0; i < 500; i++ {
s = append(s, 1)
if cap(s) != sCap {
fmt.Println(sCap)
sCap = cap(s)
}
}
}
输出
0
1
2
4
8
16
32
64
128
256
512
1024
1280
1704
2560
3584
4608
可以发现当容<1024时,以2倍递增。容量超过1024时,容量变为原来的1.25倍,当原切片的长度(以下简称原长度)大于或等于1024时,Go 语言将会以原容量的 1.25倍作为新容量的基准(以下新容量基准)。新容量基准会被调整(不断地与1.25相乘),直到结果不小于原长度与要追加的元素数量之和(以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的
另外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容 量就会以新长度为基准。注意,与前面那种情况一样,最终的新容量在很多时候都要比新容 量基准更大一些。更多细节可参见runtime包中 slice.go 文件里的growslice及相关函数 的具体实现。
append 的实现只是简单的在内存中将旧 slice 复制给新 slice
type slice struct {
array unsafe.Pointer
len int
cap int
}
func growslice(et *_type, old slice, cap int) slice {
...
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
}
}
}
...
}
更新
修改函数,观察在slice容量变化时内存地址的变化情况
func main() {
var s []int
var sCap int
for i := 0; i < 100; i++ {
fmt.Printf("addr: %p\t cap: %v\t len: %v \n", s, sCap, len(s))
s = append(s, 1)
sCap = cap(s)
}
}
输出
addr: 0x0 cap: 0 len: 0
addr: 0xc000016090 cap: 1 len: 1
addr: 0xc000098000 cap: 2 len: 2
addr: 0xc0000180a0 cap: 4 len: 3
addr: 0xc0000180a0 cap: 4 len: 4
addr: 0xc00001c080 cap: 8 len: 5
addr: 0xc00001c080 cap: 8 len: 6
addr: 0xc00001c080 cap: 8 len: 7
addr: 0xc00001c080 cap: 8 len: 8
addr: 0xc00009c000 cap: 16 len: 9
addr: 0xc00009c000 cap: 16 len: 10
addr: 0xc00009c000 cap: 16 len: 11
addr: 0xc00009c000 cap: 16 len: 12
addr: 0xc00009c000 cap: 16 len: 13
addr: 0xc00009c000 cap: 16 len: 14
addr: 0xc00009c000 cap: 16 len: 15
addr: 0xc00009c000 cap: 16 len: 16
addr: 0xc00009e000 cap: 32 len: 17
...
当容量变化时,地址也发生了变化,它不再是原来的旧切片。
之所以生成了新的切片,是因为原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。