深入理解go的slice和到底什么時(shí)候該用slice

前言

用過go語(yǔ)言的親們都知道悬槽,slice(中文翻譯為切片)在編程中經(jīng)常用到踊谋,它代表變長(zhǎng)的序列立由,序列中每個(gè)元素都有相同的類型筝家,類似一個(gè)動(dòng)態(tài)數(shù)組洼裤,利用append可以實(shí)現(xiàn)動(dòng)態(tài)增長(zhǎng),利用slice的特性可以很容易的切割slice溪王,它們是怎么實(shí)現(xiàn)這些特性的呢腮鞍?現(xiàn)在我們來探究一下這些特性的本質(zhì)是什么。

先了解一下slice的特性

定義一個(gè)slice:

s := []int{1,2,3,4,5}
fmt.Println(s)  // [1 2 3 4 5]

一個(gè)slice類型一般寫作[]T莹菱,其中T代表slice中元素的類型移国;slice的語(yǔ)法和數(shù)組很像,只是沒有固定長(zhǎng)度而已道伟。

slice的擴(kuò)容:

s := []int{1,2,3,4,5}
s = append(s, 6)
fmt.Println(s)  // [1 2 3 4 5 6]

內(nèi)置append函數(shù)在現(xiàn)有數(shù)組的長(zhǎng)度 < 1024 時(shí) cap 增長(zhǎng)是翻倍的迹缀,再往上的增長(zhǎng)率則是 1.25,至于為何后面會(huì)說蜜徽。

slice的切割:

s := []int{1,2,3,4,5,6}
s1 := s[0:2]
fmt.Println(s1)  // [1 2]
s2 := s[4:]
fmt.Println(s2)  // [5 6]
s3 := s[:4]
fmt.Println(s3)  // [1 2 3 4]

slice作為函數(shù)參數(shù):

package main

import "fmt"

func main() {

    slice_1 := []int{1, 2, 3, 4, 5}
    fmt.Printf("main-->data:\t%#v\n", slice_1)
    fmt.Printf("main-->len:\t%#v\n", len(slice_1))
    fmt.Printf("main-->cap:\t%#v\n", cap(slice_1))
    test1(slice_1)
    fmt.Printf("main-->data:\t%#v\n", slice_1)

    test2(&slice_1)
    fmt.Printf("main-->data:\t%#v\n", slice_1)

}

func test1(slice_2 []int) {
    slice_2[1] = 6666               // 函數(shù)外的slice確實(shí)有被修改
    slice_2 = append(slice_2, 8888) // 函數(shù)外的不變
    fmt.Printf("test1-->data:\t%#v\n", slice_2)
    fmt.Printf("test1-->len:\t%#v\n", len(slice_2))
    fmt.Printf("test1-->cap:\t%#v\n", cap(slice_2))
}

func test2(slice_2 *[]int) { // 這樣才能修改函數(shù)外的slice
    *slice_2 = append(*slice_2, 6666)
}

結(jié)果:

main-->data:    []int{1, 2, 3, 4, 5}
main-->len: 5
main-->cap: 5
test1-->data:   []int{1, 6666, 3, 4, 5, 8888}
test1-->len:    6
test1-->cap:    12
main-->data:    []int{1, 6666, 3, 4, 5}
main-->data:    []int{1, 6666, 3, 4, 5, 6666}

這里要注意注釋的地方祝懂,為何slice作為值傳遞參數(shù),函數(shù)外的slice也被更改了拘鞋?為何在函數(shù)內(nèi)append不能改變函數(shù)外的slice砚蓬?要回答這些問題就得了解slice內(nèi)部結(jié)構(gòu),詳細(xì)請(qǐng)看下面.

slice的內(nèi)部結(jié)構(gòu)

其實(shí)slice在Go的運(yùn)行時(shí)庫(kù)中就是一個(gè)C語(yǔ)言動(dòng)態(tài)數(shù)組的實(shí)現(xiàn)盆色,在$GOROOT/src/pkg/runtime/runtime.h中可以看到它的定義:

struct    Slice
    {    // must not move anything
        byte*    array;        // actual data
        uintgo    len;        // number of elements
        uintgo    cap;        // allocated number of elements
    };

這個(gè)結(jié)構(gòu)有3個(gè)字段怜械,第一個(gè)字段表示array的指針颅和,就是真實(shí)數(shù)據(jù)的指針(這個(gè)一定要注意),所以才經(jīng)常說slice是數(shù)組的引用缕允,第二個(gè)是表示slice的長(zhǎng)度峡扩,第三個(gè)是表示slice的容量,注意:len和cap都不是指針障本。

現(xiàn)在就可以解釋前面的例子slice作為函數(shù)參數(shù)提出的問題:

函數(shù)外的slice叫slice_1教届,函數(shù)的參數(shù)叫slice_2,當(dāng)函數(shù)傳遞slice_1的時(shí)候驾霜,其實(shí)傳入的確實(shí)是slice_1參數(shù)的復(fù)制案训,所以slice_2復(fù)制了slise_1,但要注意的是slice_2里存儲(chǔ)的數(shù)組的指針粪糙,所以當(dāng)在函數(shù)內(nèi)更改數(shù)組內(nèi)容時(shí)强霎,函數(shù)外的slice_1的內(nèi)容也改變了。在函數(shù)內(nèi)用append時(shí)蓉冈,append會(huì)自動(dòng)以倍增的方式擴(kuò)展slice_2的容量城舞,但是擴(kuò)展也僅僅是函數(shù)內(nèi)slice_2的長(zhǎng)度和容量,slice_1的長(zhǎng)度和容量是沒變的寞酿,所以在函數(shù)外打印時(shí)看起來就是沒變家夺。

append的運(yùn)作機(jī)制

在對(duì)slice進(jìn)行append等操作時(shí),可能會(huì)造成slice的自動(dòng)擴(kuò)容伐弹。其擴(kuò)容時(shí)的大小增長(zhǎng)規(guī)則是:

  • 如果新的slice大小是當(dāng)前大小2倍以上拉馋,則大小增長(zhǎng)為新大小

  • 否則循環(huán)以下操作:如果當(dāng)前slice大小小于1024,按每次2倍增長(zhǎng)惨好,否則每次按當(dāng)前大小1/4增長(zhǎng)煌茴。直到增長(zhǎng)的大小超過或等于新大小。

  • append的實(shí)現(xiàn)只是簡(jiǎn)單的在內(nèi)存中將舊slice復(fù)制給新slice

至于為何會(huì)這樣日川,你要看一下golang的源碼slice就知道了:

newcap := old.cap
if newcap+newcap < cap {
    newcap = cap
} else {
    for {
        if old.len < 1024 {
            newcap += newcap
        } else {
            newcap += newcap / 4
        }
        if newcap >= cap {
            break
        }
    }
}

為何不用動(dòng)態(tài)鏈表實(shí)現(xiàn)slice蔓腐?

  • 首先拷貝一斷連續(xù)的內(nèi)存是很快的,假如不想發(fā)生拷貝逗鸣,也就是用動(dòng)態(tài)鏈表,那你就沒有連續(xù)內(nèi)存绰精。此時(shí)隨機(jī)訪問開銷會(huì)是:鏈表 O(N), 2倍增長(zhǎng)塊鏈 O(LogN),二級(jí)表一個(gè)常數(shù)很大的O(1)撒璧。問題不僅是算法上開銷,還有內(nèi)存位置分散而對(duì)緩存高度不友好笨使,這些問題i在連續(xù)內(nèi)存方案里都是不存在的卿樱。除非你的應(yīng)用是狂append然后只順序讀一次,否則優(yōu)化寫而犧牲讀都完全不 make sense. 而就算你的應(yīng)用是嚴(yán)格順序讀硫椰,緩存命中率也通常會(huì)讓你的綜合效率比拷貝換連續(xù)內(nèi)存低繁调。

  • 對(duì)小 slice 來說萨蚕,連續(xù) append 的開銷更多的不是在 memmove, 而是在分配一塊新空間的 memory allocator 和之后的 gc 壓力(這方面對(duì)鏈表更是不利)。所以蹄胰,當(dāng)你能大致知道所需的最大空間(在大部分時(shí)候都是的)時(shí)岳遥,在make的時(shí)候預(yù)留相應(yīng)的 cap 就好。如果所需的最大空間很大而每次使用的空間量分布不確定裕寨,那你就要在浪費(fèi)內(nèi)存和耗 CPU 在 allocator + gc 上做權(quán)衡浩蓉。

  • Go 在 append 和 copy 方面的開銷是可預(yù)知+可控的,應(yīng)用上簡(jiǎn)單的調(diào)優(yōu)有很好的效果宾袜。這個(gè)世界上沒有免費(fèi)的動(dòng)態(tài)增長(zhǎng)內(nèi)存捻艳,各種實(shí)現(xiàn)方案都有設(shè)計(jì)權(quán)衡。

什么時(shí)候該用slice庆猫?

在go語(yǔ)言中slice是很靈活的认轨,大部分情況都能表現(xiàn)的很好,但也有特殊情況月培。
當(dāng)程序要求slice的容量超大并且需要頻繁的更改slice的內(nèi)容時(shí)嘁字,就不應(yīng)該用slice,改用list更合適节视。

轉(zhuǎn)自: https://segmentfault.com/a/1190000005812839

后記

  • slice的底層是array拳锚,make([]T, len, cap)的動(dòng)作是: 1. 初始化一個(gè)array;2. 返回一個(gè)指向1的slice
  • slice的數(shù)據(jù)結(jié)構(gòu)中ptr至關(guān)重要寻行,它影響slice1 = slice2賦值過程霍掺,二者的底層數(shù)據(jù)結(jié)構(gòu)是同一份數(shù)據(jù)的拷貝(包含*ptr, len, cap)
    • 其中*ptr相同,意味著不同slice最終指向的array元素是相同的地址拌蜘。
    • 舉例: slice2 = slice1[2:]杆烁,其中&slice1[2] == &slice2[0]
  • append函數(shù)是copy了連續(xù)內(nèi)存中的值到另一段連續(xù)內(nèi)存
    • 舉例: slice2 = append(slice1, 3), 其中 &slice1[0] != &slice2[0]
    • 以上的舉例不完全正確~~
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市简卧,隨后出現(xiàn)的幾起案子兔魂,更是在濱河造成了極大的恐慌,老刑警劉巖举娩,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件析校,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡铜涉,警方通過查閱死者的電腦和手機(jī)智玻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芙代,“玉大人吊奢,你說我怎么就攤上這事∥婆耄” “怎么了页滚?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵召边,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我裹驰,道長(zhǎng)隧熙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任邦马,我火速辦了婚禮贱鼻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘滋将。我一直安慰自己邻悬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布随闽。 她就那樣靜靜地躺著父丰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掘宪。 梳的紋絲不亂的頭發(fā)上蛾扇,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音魏滚,去河邊找鬼镀首。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鼠次,可吹牛的內(nèi)容都是我干的更哄。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼腥寇,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼成翩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赦役,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤麻敌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后掂摔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體术羔,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年乙漓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了级历。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡簇秒,死狀恐怖鱼喉,靈堂內(nèi)的尸體忽然破棺而出秀鞭,到底是詐尸還是另有隱情趋观,我是刑警寧澤扛禽,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站皱坛,受9級(jí)特大地震影響编曼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜剩辟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一掐场、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贩猎,春花似錦熊户、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至艇棕,卻和暖如春蝌戒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背沼琉。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工北苟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人打瘪。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓友鼻,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親瑟慈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子桃移,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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