切片的容量
[5]int
是數(shù)組,而 []int
是切片性宏。二者看起來相似阁危,實則是根本上不同的數(shù)據(jù)結(jié)構(gòu)玛痊。
切片的數(shù)據(jù)結(jié)構(gòu)中,包含一個指向數(shù)組的指針 array
狂打,當前長度 len
擂煞,以及最大容量 cap
。在使用 make([]int, len)
創(chuàng)建切片時趴乡,實際上還有第三個可選參數(shù) cap
对省,也即 make([]int, len, cap)
。在不聲明 cap
的情況下晾捏,默認 cap=len
蒿涎。當切片長度沒有超過容量時,對切片新增數(shù)據(jù)惦辛,不會改變 array
指針的值劳秋。
當對切片進行 append
操作,導致長度超出容量時胖齐,就會創(chuàng)建新的數(shù)組玻淑,這會導致和原有切片的分離。在下例中
a := make([]int, 5)
b := a[0:4]
a = append(a, 1)
a[1] = 5
fmt.Println(b)
// [0 0 0 0]
fmt.Println(a)
// [0 5 0 0 0 1]</pre>
由于 a
的長度超出了容量呀伙,所以切片 a
指向了一個增長后的新數(shù)組补履,而 b
仍然指向原來的老數(shù)組。所以之后對 a
進行的操作剿另,對 b
不會產(chǎn)生影響箫锤。
試比較
a := make([]int, 5, 6)
b := a[0:4]
a = append(a, 1)
a[1] = 5
fmt.Println(a, b)
// [0 5 0 0 0 1] [0 5 0 0]
本例中贬蛙, a
的容量為6,因此在 append
后并未超出容量谚攒,所以 array
指針沒有改變阳准。因此,對 a
進行的操作五鲫,對 b
同樣產(chǎn)生了影響溺职。
擴容機制
下面看看用 a := []int{}
這種方式來創(chuàng)建切片會是什么情況。
a := []int{}
for i := 0; i < 16; i++ {
a = append(a, i)
fmt.Print(cap(a), " ")
}
// 1 2 4 4 8 8 8 8 16 16 16 16 16 16 16 16
可以看到位喂,空切片的容量為0浪耘,但后面向切片中添加元素時,并不是每次切片的容量都發(fā)生了變化塑崖。這是因為七冲,如果增大容量,也即需要創(chuàng)建新數(shù)組规婆,這時還需要將原數(shù)組中的所有元素復制到新數(shù)組中澜躺,開銷很大,所以GoLang設計了一套擴容機制抒蚜,以減少需要創(chuàng)建新數(shù)組的次數(shù)掘鄙。但這導致無法很直接地判斷 append
時是否創(chuàng)建了新數(shù)組。
如果一次添加多個元素嗡髓,容量又會怎樣變化呢操漠?試比較下面兩個例子:
a := []int{}
for i := 0; i < 16; i++ {
a = append(a, 1, 2, 3, 4, 5)
fmt.Print(cap(a), " ")
}
// 6 12 24 24 48 48 48 48 48 96 96 96 96 96 96 96 </pre>
<pre class="cm-s-default" style="box-sizing: border-box; font-size: inherit; font-family: inherit; margin: 0px; overflow: visible; padding: 0px; border-radius: 0px; border-width: 0px; background: transparent; white-space: pre; overflow-wrap: normal; line-height: inherit; color: inherit; z-index: 2; position: relative; -webkit-tap-highlight-color: transparent; font-variant-ligatures: contextual;">a := []int{}
for i := 0; i < 16; i++ {
a = append(a, 1, 2, 3, 4, 5, 6)
fmt.Print(cap(a), " ")
}
// 6 12 24 24 48 48 48 48 96 96 96 96 96 96 96 96
那么,是不是說饿这,當向一個空切片中插入 2n-1
個元素時浊伙,容量就會被設置為 2n
呢?我們來試試其他的數(shù)據(jù)類型长捧。
// int8
a := []int8{}
for i := 0; i < 16; i++ {
a = append(a, 1, 2, 3, 4, 5, 6)
fmt.Print(cap(a), " ")
}
// 8 16 32 32 32 64 64 64 64 64 128 128 128 128 128 128
// int16
fmt.Println()
b := []int16{}
for i := 0; i < 16; i++ {
b = append(b, 1, 2, 3, 4, 5)
fmt.Print(cap(b), " ")
}
// 8 16 32 32 32 64 64 64 64 64 128 128 128 128 128 128
// bool
fmt.Println()
c := []bool{}
for i := 0; i < 16; i++ {
c = append(c, true, false, true, false, false)
fmt.Print(cap(c), " ")
}
// 8 16 32 32 32 64 64 64 64 64 128 128 128 128 128 128
// float32
fmt.Println()
d := []float32{}
for i := 0; i < 16; i++ {
d = append(d, 1.1, 2.2, 3.3, 4.4, 5.5)
fmt.Print(cap(d), " ")
}
// 8 16 16 32 32 32 64 64 64 64 64 64 128 128 128 128
// float64
fmt.Println()
e := []float64{}
for i := 0; i < 16; i++ {
e = append(e, 1.1, 2.2, 3.3, 4.4, 5.5)
fmt.Print(cap(e), " ")
}
// 6 12 24 24 48 48 48 48 48 96 96 96 96 96 96 96
// string
fmt.Println()
f := []string{}
for i := 0; i < 16; i++ {
f = append(f, "1.1", "2.2", "3.3", "4.4", "5.5")
fmt.Print(cap(f), " ")
}
// 5 10 20 20 40 40 40 40 80 80 80 80 80 80 80 80
// []int
fmt.Println()
g := [][]int{}
g1 := []int{1, 2, 3, 4, 5}
for i := 0; i < 16; i++ {
g = append(g, g1, g1, g1, g1, g1)
fmt.Print(cap(g), " ")
}
// 5 10 20 20 42 42 42 42 85 85 85 85 85 85 85 85
可以看到嚣鄙,根據(jù)切片對應數(shù)據(jù)類型的不同,容量增長的方式也有很大的區(qū)別串结。相關(guān)的源碼包括:src/runtime/msize.go哑子,src/runtime/mksizeclasses.go等。
我們再看看切片初始非空的情形肌割。
a := []int{1, 2, 3}
fmt.Println(cap(a))
// 3
for i := 0; i < 16; i++ {
a = append(a, 1, 2)
fmt.Print(cap(a), " ")
}
// 6 12 12 12 24 24 24 24 24 24 48 48 48 48 48 48
可以看到卧蜓,與剛剛向空切片添加5個int的情況一致,向有3個int的切片中添加2個int声功,容量增長為6。
需要注意的是宠叼,append
對切片擴容時先巴,如果容量超過了一定范圍其爵,處理策略又會有所不同∩祢牵可以看看下面這個例子摩渺。
a := []int{1, 2, 3, 4}
fmt.Println(cap(a))
// 4
for i := 0; i < 20; i++ {
a = append(a, a...)
fmt.Print(cap(a), " ")
}
// 8 16 32 64 128 256 512 1024 2560 5120 10240 20480 40960 80896 158720 310272 606208 1184768 2314240 4520960
具體為什么會是這樣的變化過程,還需要從源碼中尋找答案剂邮。下面是 src/runtime/slice.go
中的 growslice
函數(shù)中的核心部分摇幻。
// src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
// ...省略部分
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
}
}
}
// ...省略部分
}
- 當需要的容量超過原切片容量的兩倍時,會使用需要的容量作為新容量挥萌。
- 當原切片長度小于1024時绰姻,新切片的容量會直接翻倍。而當原切片的容量大于等于1024時引瀑,會反復地增加25%狂芋,直到新容量超過所需要的容量。
結(jié)論
GoLang中的切片擴容機制憨栽,與切片的數(shù)據(jù)類型帜矾、原本切片的容量、所需要的容量都有關(guān)系屑柔,比較復雜屡萤。對于常見數(shù)據(jù)類型,在元素數(shù)量較少時掸宛,大致可以認為擴容是按照翻倍進行的死陆。但具體情況需要具體分析。
為了避免因為切片是否發(fā)生擴容的問題導致bug旁涤,最好的處理辦法還是在必要時使用
copy
來復制數(shù)據(jù)翔曲,保證得到一個新的切片,以避免后續(xù)操作帶來預料之外的副作用劈愚。