Golang slice 的底層實(shí)現(xiàn)

首先我們來(lái)看段代碼的輸出

    s := make([][]int, 4)
    for i := 0; i < 4; i++ {
        s[i] = make([]int, 4)
        s[i][0] = 1
    }

    s0 := s[0]
    s0 = append(s0, 5)
    fmt.Println(s)

輸出的結(jié)果是

[[1 0 0 0] [1 0 0 0] [1 0 0 0] [1 0 0 0]]

append的值5并沒(méi)有輸出,那么究竟是s0并不等價(jià)于s[0],還是有其他原因呢?
首先,肯定的是在Go中,所有的拷貝都是值拷貝,不存在引用拷貝;
其次,slice由一個(gè)指針,兩個(gè)整型(分別是size和cap)來(lái)實(shí)現(xiàn),既然s0包含的指針和s[0]包含的指針相同,為什么append操作并沒(méi)有達(dá)到預(yù)期效果呢?

帶著這些疑問(wèn),我們來(lái)看看slice的底層實(shí)現(xiàn)。

slice的結(jié)構(gòu)

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

array指向一個(gè)數(shù)組元素的地址,這個(gè)數(shù)組可能在makeslice時(shí)創(chuàng)建,也可能之前就存在,而slice被"attach"上去,例如 s := a[0:5];

//代碼比較簡(jiǎn)單
func makeslice(et *_type, len, cap int) slice {
   maxElements := maxSliceCap(et.size)
   
   if len < 0 || uintptr(len) > maxElements {
       panic(errorString("makeslice: len out of range"))
   }

   if cap < len || uintptr(cap) > maxElements {
       panic(errorString("makeslice: cap out of range"))
   }
   //分配數(shù)組空間
   p := mallocgc(et.size*uintptr(cap), et, true)
   return slice{p, len, cap}
}

e.g.

s := make([]int, 1, 3)
fmt.Printf("%p, %v, len:%d, cap:%d", unsafe.Pointer(&s[0]), s, len(s), cap(s))

輸出:

0xc42007c0a0, [0], len:1, cap:3

對(duì)于直接"attach"到數(shù)組的情形,類似下面這樣

a := [10]int{0,1,2,3,4,5,6,7,8,9}

fmt.Printf("%p, %v \n", &a, a)

s1 := a[1:5:7]

fmt.Printf("%p, %v, len:%d, cap:%d, self:%p \n", unsafe.Pointer(&s1[0]), s1, len(s1), cap(s1), unsafe.Pointer(&s1) )

輸出:

0xc42006e050, [0 1 2 3 4 5 6 7 8 9] 
0xc42006e058, [1 2 3 4], len:4, cap:6, self:0xc42006c140

根據(jù)兩個(gè)指針的關(guān)系,可以看出,slice直接指向了數(shù)組中的元素


slice attach1

同理,slice2還可以通過(guò)另一個(gè)slice1構(gòu)造,但其屬性依賴slice1,并不是slice1底層的數(shù)組

s2 := s1[2:]
fmt.Printf("%p, %v, len:%d, cap:%d, self:%p \n", unsafe.Pointer(&s2[0]), s2, len(s2), cap(s2), unsafe.Pointer(&s2) )

輸出

0xc42006e068, [3 4], len:2, cap:4, self:0xc42006a140 
slice attach2

修改slice中元素的值,實(shí)際上修改的是底層數(shù)組元素的值

s2[0] = 100
fmt.Println(a, s1, s2)

輸出

[0 1 2 100 4 5 6 7 8 9] [1 2 100 4] [100 4]

擴(kuò)張

當(dāng)我們對(duì)slice進(jìn)行append操作時(shí),若len + 追加元素個(gè)數(shù) <= cap時(shí),不會(huì)發(fā)生內(nèi)存擴(kuò)張;否則,新的內(nèi)存被申請(qǐng),同時(shí)舊的數(shù)據(jù)被拷貝至新內(nèi)存的前部;

先上代碼

func growslice(et *_type, old slice, cap int) slice {
    ......

    if et.size == 0 {
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }
        
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }
    // 計(jì)算新的cap,針對(duì)不同情況分別處理
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            //2倍
            newcap = doublecap
        } else {
            //每次嘗試擴(kuò)1/4
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }

    var lenmem, newlenmem, capmem uintptr
    const ptrSize = unsafe.Sizeof((*byte)(nil))
    switch et.size {
    case 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case ptrSize:
        lenmem = uintptr(old.len) * ptrSize
        newlenmem = uintptr(cap) * ptrSize
        capmem = roundupsize(uintptr(newcap) * ptrSize)
        newcap = int(capmem / ptrSize)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem = roundupsize(uintptr(newcap) * et.size)
        newcap = int(capmem / et.size)
    }
    ......
    var p unsafe.Pointer
    if et.kind&kindNoPointers != 0 {
        //這里有個(gè)優(yōu)化細(xì)節(jié),先不zero,因?yàn)榍安繒?huì)發(fā)生覆蓋
        p = mallocgc(capmem, nil, false)
        memmove(p, old.array, lenmem)
        //只對(duì)后部zero
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        p = mallocgc(capmem, et, true)
        if !writeBarrier.enabled {
            memmove(p, old.array, lenmem)
        } else {
            for i := uintptr(0); i < lenmem; i += et.size {
                typedmemmove(et, add(p, i), add(old.array, i))
            }
        }
    }
    return slice{p, old.len, newcap}
}

當(dāng)不需要擴(kuò)容時(shí),append會(huì)修改底層數(shù)組的值

s2 = append(s2, 1000)
fmt.Printf("%p, %v \n", unsafe.Pointer(&s2[0]), s2)
fmt.Println(a, s1, s2)
fmt.Printf("len(s1)=%d, cap(s1)=%d, len(s2)=%d, cap(s2)=%d", len(s1), cap(s1), len(s2), cap(s2))

輸出

[0 1 2 100 4 1000 6 7 8 9] [1 2 100 4] [100 4 1000]
len(s1)=4, cap(s1)=6, len(s2)=3, cap(s2)=4

注意,對(duì)s2的append,僅改變了s2的len,并沒(méi)有改變s1的len
當(dāng)append過(guò)多時(shí),slice底層數(shù)組會(huì)發(fā)生變化

s2 = append(s2, 1000, 1001, 1002)
fmt.Printf("%p, %v \n", unsafe.Pointer(&s2[0]), s2)
fmt.Println(a, s1, s2)
fmt.Printf("len(s1)=%d, cap(s1)=%d, len(s2)=%d, cap(s2)=%d", len(s1), cap(s1), len(s2), cap(s2))

輸出

0xc420014280, [100 4 1000 1001 1002] 
[0 1 2 100 4 5 6 7 8 9] [1 2 100 4] [100 4 1000 1001 1002]
len(s1)=4, cap(s1)=6, len(s2)=5, cap(s2)=8

原數(shù)組a,切片s1的屬性未受影響;但s2底層的數(shù)組已發(fā)生變化,cap也是之前的2倍。

總結(jié)

1、多個(gè)slice指向相同的底層數(shù)組時(shí),修改其中一個(gè)slice,可能會(huì)影響其他slice的值;
2、slice作為參數(shù)傳遞時(shí),比數(shù)組更為高效,因?yàn)閟lice的結(jié)構(gòu)比較?。?br> 3、slice在擴(kuò)張時(shí),可能會(huì)發(fā)生底層數(shù)組的變更及內(nèi)存拷貝;
4、因?yàn)閟lice引用了數(shù)組,這可能導(dǎo)致數(shù)組空間不會(huì)被gc,當(dāng)數(shù)組空間很大,而slice引用內(nèi)容很少時(shí)尤為嚴(yán)重;

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 切片是 Go 中的一種基本的數(shù)據(jù)結(jié)構(gòu),使用這種結(jié)構(gòu)可以用來(lái)管理數(shù)據(jù)集合。切片的設(shè)計(jì)想法是由動(dòng)態(tài)數(shù)組概念而來(lái),為了開(kāi)...
    一縷殤流化隱半邊冰霜閱讀 11,461評(píng)論 21 55
  • 其本身并不是數(shù)組,它指向底層的數(shù)組作為變長(zhǎng)數(shù)組的替代方案,可以關(guān)聯(lián)底層數(shù)組的局部或全部為引用類型可以直接創(chuàng)建或從底...
    haokeed閱讀 272評(píng)論 0 0
  • 出處---Go編程語(yǔ)言 歡迎來(lái)到 Go 編程語(yǔ)言指南。本指南涵蓋了該語(yǔ)言的大部分重要特性 Go 語(yǔ)言的交互式簡(jiǎn)介,...
    Tuberose閱讀 18,717評(píng)論 1 46
  • Go語(yǔ)言做Web編程非常方便,并且在開(kāi)發(fā)效率和程序運(yùn)行效率方面都非常優(yōu)秀。相比于Java,其最大的優(yōu)勢(shì)就是簡(jiǎn)便易用...
    暗黑破壞球嘿哈閱讀 9,163評(píng)論 6 66
  • demonodeppt 文檔 網(wǎng)頁(yè)版PPT,適合做一些技術(shù)分享 效果: 安裝依賴 創(chuàng)建步驟 創(chuàng)建網(wǎng)頁(yè)幻燈片命令 基...
    Ray1214閱讀 2,793評(píng)論 0 1

友情鏈接更多精彩內(nèi)容