GoLang中的切片擴容機制

切片的容量

[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ù)操作帶來預料之外的副作用劈愚。

參考文獻

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瞳遍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子菌羽,更是在濱河造成了極大的恐慌掠械,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件注祖,死亡現(xiàn)場離奇詭異猾蒂,居然都是意外死亡,警方通過查閱死者的電腦和手機是晨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門肚菠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人罩缴,你說我怎么就攤上這事蚊逢〔惴觯” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵烙荷,是天一觀的道長镜会。 經(jīng)常有香客問我,道長终抽,這世上最難降的妖魔是什么戳表? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮昼伴,結(jié)果婚禮上匾旭,老公的妹妹穿的比我還像新娘。我一直安慰自己亩码,他們只是感情好季率,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著描沟,像睡著了一般飒泻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吏廉,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天泞遗,我揣著相機與錄音,去河邊找鬼席覆。 笑死史辙,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的佩伤。 我是一名探鬼主播聊倔,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼生巡!你這毒婦竟也來了耙蔑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤孤荣,失蹤者是張志新(化名)和其女友劉穎甸陌,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盐股,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡钱豁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了疯汁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牲尺。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖幌蚊,靈堂內(nèi)的尸體忽然破棺而出谤碳,到底是詐尸還是另有隱情凛澎,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布估蹄,位于F島的核電站,受9級特大地震影響沫换,放射性物質(zhì)發(fā)生泄漏臭蚁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一讯赏、第九天 我趴在偏房一處隱蔽的房頂上張望垮兑。 院中可真熱鬧,春花似錦漱挎、人聲如沸系枪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽私爷。三九已至,卻和暖如春膊夹,著一層夾襖步出監(jiān)牢的瞬間衬浑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工放刨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留工秩,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓进统,卻偏偏與公主長得像助币,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子螟碎,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 切片(slice)是 Golang 中一種比較特殊的數(shù)據(jù)結(jié)構(gòu)眉菱,這種數(shù)據(jù)結(jié)構(gòu)更便于使用和管理數(shù)據(jù)集合。切片是圍繞動態(tài)...
    小孩真笨閱讀 1,030評論 0 1
  • 切片(slice)是 Golang 中一種比較特殊的數(shù)據(jù)結(jié)構(gòu)抚芦,這種數(shù)據(jù)結(jié)構(gòu)更便于使用和管理數(shù)據(jù)集合倍谜。切片是圍繞動態(tài)...
    51reboot閱讀 28,624評論 2 10
  • 切片是 Go 中的一種基本的數(shù)據(jù)結(jié)構(gòu),使用這種結(jié)構(gòu)可以用來管理數(shù)據(jù)集合叉抡。切片的設計想法是由動態(tài)數(shù)組概念而來尔崔,為了開...
    一縷殤流化隱半邊冰霜閱讀 11,227評論 21 55
  • 數(shù)組 和C語言一樣,Go語言中也有數(shù)組的概念, Go語言中的數(shù)組也是用于保存一組相同類型的數(shù)據(jù) 和C語言一樣,Go...
    極客江南閱讀 1,199評論 0 2
  • 我在春天離開,在秋天歸來褥民。不管什么季節(jié)季春,不管什么情懷,離開有離開的理由消返,歸來有歸來的感慨载弄。 不管什么樣的處境耘拇,不管...
    別山舉水閱讀 1,358評論 13 28