golang數(shù)組和切片的底層實(shí)現(xiàn)區(qū)別和常見(jiàn)的坑

數(shù)組是由相同類型元素的集合組成的數(shù)據(jù)結(jié)構(gòu)龄减,計(jì)算機(jī)會(huì)為數(shù)組分配一塊連續(xù)的內(nèi)存來(lái)保存其中的元素赘阀,我們可以利用數(shù)組中元素的索引快速訪問(wèn)特定元素涛浙。goalng中的數(shù)組在定義時(shí)必須指定長(zhǎng)度谈竿,創(chuàng)建之后長(zhǎng)度是不可變的咸灿。因?yàn)樵跀?shù)組創(chuàng)建過(guò)程中匪燕,golang會(huì)根據(jù)定義的長(zhǎng)度去申請(qǐng)連續(xù)的內(nèi)存空間蕾羊。與其他編程語(yǔ)言一樣喧笔,數(shù)組的指針指向數(shù)組開(kāi)頭元素。

golang的切片類型是基于數(shù)組實(shí)現(xiàn)的龟再,可以理解為一個(gè)管理數(shù)組自動(dòng)擴(kuò)容的結(jié)構(gòu)书闸,具體的結(jié)構(gòu)體定義如下:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

array是一個(gè)指向數(shù)組結(jié)構(gòu)的指針,len和cap字段用于維護(hù)數(shù)組的長(zhǎng)度和容量利凑。當(dāng)我們定義一個(gè)切片類型時(shí)浆劲,切片的初始容量為0,當(dāng)我們逐漸append變量時(shí),切片會(huì)依據(jù)某種策略進(jìn)行擴(kuò)容哀澈。具體的擴(kuò)容策略比較復(fù)雜牌借,具體可以查看go/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
            }
        }
    }
    ......
}

簡(jiǎn)單來(lái)說(shuō):如果需要的cap > oldcap*2 割按,就直接分配需要的cap膨报。否則,如果oldcap< 1024, 直接加倍哲虾。如果old <= 1024丙躏,反復(fù)增加25%。直到足夠存儲(chǔ)全部?jī)?nèi)容束凑。實(shí)際上的邏輯更加復(fù)雜晒旅,最終的cap還需要根據(jù)數(shù)據(jù)類型所占的空間進(jìn)行調(diào)整,具體可以參考源碼汪诉。接下來(lái)废恋,通過(guò)如下實(shí)驗(yàn)驗(yàn)證兩種簡(jiǎn)單的情況。首先是不指定默認(rèn)長(zhǎng)度和容量時(shí)扒寄。

func TestNewSlice(T *testing.T){
    var slice []int
    fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))

    for i:=0;i<100;i++{
        slice = append(slice, i)
        fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))
    }
}

實(shí)驗(yàn)結(jié)果如下:

slice len = 0        slice cap = 0
slice len = 1        slice cap = 1
slice len = 2        slice cap = 2
slice len = 3        slice cap = 4
slice len = 4        slice cap = 4
slice len = 5        slice cap = 8
slice len = 6        slice cap = 8
slice len = 7        slice cap = 8
slice len = 8        slice cap = 8
......

接下來(lái)是通過(guò)make指定初始的長(zhǎng)度和容量時(shí)鱼鼓。

func TestMakeSlice(T *testing.T){
    var slice = make([]int,0,10)
    fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))

    for i:=0;i<100;i++{
        slice = append(slice, i)
        fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))
    }
}

實(shí)驗(yàn)結(jié)果如下:

slice len = 0        slice cap = 3
slice len = 1        slice cap = 3
slice len = 2        slice cap = 3
slice len = 3        slice cap = 3
slice len = 4        slice cap = 6
slice len = 5        slice cap = 6
slice len = 6        slice cap = 6
slice len = 7        slice cap = 12
......

因此,如果我們知道切片大致需要的容量時(shí)该编,最好通過(guò) make方法迄本,指定cap值。這樣可以有效避免數(shù)組的頻繁擴(kuò)容课竣。從而避免切片在擴(kuò)容時(shí)導(dǎo)致的性能損失嘉赎。這部分損失包括擴(kuò)容時(shí)重新申請(qǐng)內(nèi)存、數(shù)據(jù)的拷貝以及后續(xù)的垃圾回收于樟。

golang數(shù)組和切片的區(qū)別除了體現(xiàn)在是否支持?jǐn)U容外公条,還體現(xiàn)在傳值操作上。具體我們通過(guò)如下實(shí)驗(yàn)來(lái)說(shuō)明迂曲。

func TestSliceObj(t *testing.T) {

    var a1 = [3]int{1,2,3}
    fmt.Printf("the a1 = %v \t\t  a1 ptr = %p \t\t a1[0] ptr = %p\n",a1,&a1,&a1[0])

    a2 := a1
    a1[0] = 10
    
    fmt.Printf("after change the a1 = %v \t\t  a1 ptr = %p \t\t a1[0] ptr = %p\n",a1,&a1,&a1[0])
    fmt.Printf("after change the a2 = %v \t\t  a2 ptr = %p \t\t a2[0] ptr = %p\n",a2,&a2,&a2[0])
    
    var s1 = []int{1,2,3}
    fmt.Printf("the s1 = %v \t\t  s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])

    s2 := s1
    s1[0] = 10

    fmt.Printf("after change the s1 = %v \t\t  s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])
    fmt.Printf("after change the s2 = %v \t\t  s2 ptr = %p \t\t s2[0] ptr = %p\n",s2,&s2,&s2[0])
}

實(shí)驗(yàn)結(jié)果如下:

=== RUN   TestSliceObj
the a1 = [1 2 3]          a1 ptr = 0xc0000ca0a0          a1[0] ptr = 0xc0000ca0a0
after change the a1 = [10 2 3]        a1 ptr = 0xc0000ca0a0          a1[0] ptr = 0xc0000ca0a0
after change the a2 = [1 2 3]         a2 ptr = 0xc0000ca0e0          a2[0] ptr = 0xc0000ca0e0
the s1 = [1 2 3]          s1 ptr = 0xc0000b40a0          s1[0] ptr = 0xc0000ca140
after change the s1 = [10 2 3]        s1 ptr = 0xc0000b40a0          s1[0] ptr = 0xc0000ca140
after change the s2 = [10 2 3]        s2 ptr = 0xc0000b40e0          s2[0] ptr = 0xc0000ca140
--- PASS: TestSliceObj (0.00s)

通過(guò)實(shí)驗(yàn)結(jié)果我們發(fā)現(xiàn)靶橱,對(duì)于數(shù)組array來(lái)說(shuō),數(shù)組的指針和數(shù)組第一個(gè)元素的第一個(gè)指針是相同的,說(shuō)明數(shù)組指針確實(shí)指向數(shù)組第一個(gè)元素地址关霸。接著我們將a1值賦值給a2并修改a1的值传黄。我們發(fā)現(xiàn)a1和a2分別指向不同的內(nèi)存空間,同時(shí)對(duì)a1的修改不會(huì)影響a2谒拴。說(shuō)明賦值過(guò)程是值傳遞尝江,在賦值過(guò)程中會(huì)重新申請(qǐng)一塊空間。

對(duì)于切片slice來(lái)說(shuō)英上,切片指針指向slice struct的位置炭序,而切片第一個(gè)元素的地址位才是底層數(shù)組真正的地址位。然后我們將s1賦值給s2苍日,我們發(fā)現(xiàn)是s1和s2指針?lè)謩e指向兩個(gè)不同的空間惭聂,說(shuō)明該賦值過(guò)程同樣也是值傳遞,即當(dāng)前內(nèi)存中存在兩個(gè)slice結(jié)構(gòu)體對(duì)象,分別為s1和s2相恃。與數(shù)組不同的是辜纲,兩個(gè)切片所對(duì)應(yīng)的底層數(shù)組是同一個(gè)。當(dāng)我們修改s1的值之后拦耐,s2也同樣發(fā)生了變化耕腾。

那么這是不是就說(shuō)明,golang中切片類型的賦值是指針復(fù)制或者說(shuō)是淺拷貝呢杀糯?

答案是否定的扫俺,接下來(lái)介紹一個(gè)golang切片使用過(guò)程中常見(jiàn)的坑。首先做一組實(shí)驗(yàn)固翰。

func TestAppendSlice(T *testing.T){
    var s1 = []int{1,2,3,4}
    fmt.Printf("the s1 = %v \t\t  s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])

    s2 := s1
    s1 = append(s1, 5)

    fmt.Printf("after change the s1 = %v \t\t  s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])
    fmt.Printf("after change the s2 = %v \t\t  s2 ptr = %p \t\t s2[0] ptr = %p\n",s2,&s2,&s2[0])
}

實(shí)驗(yàn)結(jié)果如下:

=== RUN   TestAppendSlice
the s1 = [1 2 3 4]        s1 ptr = 0xc00000c0e0          s1[0] ptr = 0xc000018200
after change the s1 = [1 2 3 4 5]         s1 ptr = 0xc00000c0e0          s1[0] ptr = 0xc00001a180
after change the s2 = [1 2 3 4]           s2 ptr = 0xc00000c120          s2[0] ptr = 0xc000018200
--- PASS: TestAppendSlice (0.00s)

通過(guò)實(shí)驗(yàn)結(jié)果我們發(fā)現(xiàn)狼纬,當(dāng)我們將s1賦值給s2之后,再修改s1骂际,s2并未像之前一樣也發(fā)生變化疗琉。另外我們發(fā)現(xiàn)s2對(duì)應(yīng)的底層數(shù)組與s1是相同的,這和之前的實(shí)驗(yàn)結(jié)論一致歉铝。不同之處在于盈简,s1在修改后指針指向的底層數(shù)組地址發(fā)生了變化,這是因?yàn)閍ppend操作恰好出發(fā)了一次數(shù)組擴(kuò)容太示,從而導(dǎo)致切片重新申請(qǐng)了一塊連續(xù)地址柠贤。

因此在使用切片時(shí)一定注意這一點(diǎn),尤其是當(dāng)發(fā)生參數(shù)傳遞時(shí)先匪,需要確定自己期待的是值傳遞還是指針傳遞。否則可能會(huì)遇到難以解釋的bug弃衍。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末呀非,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌岸裙,老刑警劉巖猖败,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異降允,居然都是意外死亡恩闻,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)剧董,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)幢尚,“玉大人,你說(shuō)我怎么就攤上這事翅楼∥臼#” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵毅臊,是天一觀的道長(zhǎng)理茎。 經(jīng)常有香客問(wèn)我,道長(zhǎng)管嬉,這世上最難降的妖魔是什么皂林? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮蚯撩,結(jié)果婚禮上础倍,老公的妹妹穿的比我還像新娘。我一直安慰自己求厕,他們只是感情好著隆,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著呀癣,像睡著了一般美浦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上项栏,一...
    開(kāi)封第一講書(shū)人閱讀 49,950評(píng)論 1 291
  • 那天浦辨,我揣著相機(jī)與錄音,去河邊找鬼沼沈。 笑死流酬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的列另。 我是一名探鬼主播芽腾,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼页衙!你這毒婦竟也來(lái)了摊滔?” 一聲冷哼從身側(cè)響起阴绢,我...
    開(kāi)封第一講書(shū)人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎艰躺,沒(méi)想到半個(gè)月后呻袭,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡腺兴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年左电,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片页响。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡篓足,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拘泞,到底是詐尸還是另有隱情纷纫,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布陪腌,位于F島的核電站辱魁,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏诗鸭。R本人自食惡果不足惜染簇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望强岸。 院中可真熱鬧锻弓,春花似錦、人聲如沸蝌箍。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)妓盲。三九已至杂拨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間悯衬,已是汗流浹背弹沽。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留筋粗,地道東北人策橘。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像娜亿,于是被迫代替她去往敵國(guó)和親丽已。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

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

  • Slice和數(shù)組概念定義以及區(qū)別 數(shù)組 數(shù)組是一個(gè)由固定長(zhǎng)度的特定類型元素組成的序列辰斋,一個(gè)數(shù)組可以由零個(gè)或多個(gè)元素...
    亦一銀河閱讀 547評(píng)論 0 0
  • 數(shù)組是指一系列同一類型的數(shù)據(jù)集合旁仿。數(shù)組中包含的每個(gè)數(shù)據(jù)被稱為數(shù)組元素,數(shù)組中元素的個(gè)數(shù)孽糖,稱為數(shù)組的長(zhǎng)度枯冈。 數(shù)組的創(chuàng)...
    水無(wú)寒閱讀 1,169評(píng)論 0 0
  • 數(shù)組類型的值(以下簡(jiǎn)稱數(shù)組)的長(zhǎng)度是固定的,而切片類型的值(以下簡(jiǎn)稱切片)是可變長(zhǎng)的办悟。 ?數(shù)組的長(zhǎng)度在聲明它的時(shí)候...
    one_zheng閱讀 1,174評(píng)論 0 3
  • 數(shù)組的聲明 變量的聲明已經(jīng)講過(guò)啦,不熟悉的可以看第二章 數(shù)組的遍歷 兩種方法,一種傳統(tǒng)的下標(biāo)遍歷一種上一章講到的r...
    神奇大葉子閱讀 357評(píng)論 0 0
  • 表情是什么尘奏,我認(rèn)為表情就是表現(xiàn)出來(lái)的情緒。表情可以傳達(dá)很多信息病蛉。高興了當(dāng)然就笑了炫加,難過(guò)就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 124,525評(píng)論 2 7