Go語言-Slice詳解

来源:https://www.cnblogs.com/yanlishao/archive/2023/08/31/17670512.html
-Advertisement-
Play Games

Go語言中的slice表示一個具有相同類型元素的可變長序列,語言本身提供了兩個操作方法: 1. 創建:make([]T,len,cap) 2. 追加: append(slice, T ...) 同時slice支持隨機訪問。本篇文章主要對slice的具體實現進行總結。 ## 1. 數據結構 go語言的 ...


Go語言中的slice表示一個具有相同類型元素的可變長序列,語言本身提供了兩個操作方法:

  1. 創建:make([]T,len,cap)
  2. 追加: append(slice, T ...)

同時slice支持隨機訪問。本篇文章主要對slice的具體實現進行總結。

1. 數據結構

go語言的slice有三個主要的屬性:

  1. 指針:slice的首地址指針
  2. 長度:slice中元素的個數
  3. 容量:由於slice底層結構本身物理空間可能更大,因此該值記錄slice實際空間大小。

因此,在golang官網中的Go Slices: usage and internals對slice的描述如下:

A slice is a descriptor of an array segment. It consists of a pointer to the array, the length of the segment, and its capacity (the maximum length of the segment).

slice是一段array,包括了上面的三個部分,他的物理結構如下:

如果我們通過make([]byte,5,5)創建了一個len=5,cap=5的slice,其物理結構如此:

如果我們僅僅想使用原數組的一部分,例如:

s = s[2:4]

則s的物理結構如此:

但實際上,這兩者所引用的是同一塊連續的空間,如果我們修改其中一個,另一個也會跟著修改。實際上,slice在go語言中的代碼表示為:

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

我們是如何知道這件事的呢?請看繼續閱讀該文章。

操作

go語言為slice提供了兩個修改類操作:

  1. 創建
  2. 追加

接下來我們會對這兩個操作進行分析。

1. 創建slice

slice的定義(分配空間)有三種方式:

  1. 字面量創建:s := []int{1,2,3}
  2. 內置函數make創建:make([]T, len, cap)
  3. 切取其他數據結構:s := array[1:2]

還有兩種聲明方式(不分配空間):

  1. var s []int
  2. s := []int{}

接下來我們通過一組示例代碼,查看slice的創建流程,以及上面的定義與聲明的區別。

  1. 字面量創建方式

    // main.go
    package main
    
    import "fmt"
    
    func main() {
        s1 := []int{1,2,3}
        fmt.Println(s1)
    }
    

    這組代碼給出了一個通過字面量方式創建的slice s1,我們通過delve工具對這部分代碼進行debug。命令行進入到main.go所在目錄,鍵入如下命令:

    dlv debug
    
    # 為main包的main函數第1行即文件第7行打上斷點
    b main.go:7
    
    # 運行到斷點處
    c
    
    # 對要運行的部分進行反彙編
    disassemble
    

    我們就可以看到如下代碼:

    TEXT main.main(SB) D:/code/Notes/docs/go/list/main.go
            main.go:6       0x948300        493b6610                cmp rsp, qword ptr [r14+0x10]
            main.go:6       0x948304        0f86f5000000            jbe 0x9483ff
            main.go:6       0x94830a        4883ec78                sub rsp, 0x78
            main.go:6       0x94830e        48896c2470              mov qword ptr [rsp+0x70], rbp
            main.go:6       0x948313        488d6c2470              lea rbp, ptr [rsp+0x70]
    =>      main.go:7       0x948318*       488d05a1940000          lea rax, ptr [rip+0x94a1]
            main.go:7       0x94831f        90                      nop
            # 調用runtime的newobject創建一個新的對象
            main.go:7       0x948320        e81b53f6ff              call $runtime.newobject
            # 將調用結果(即新slice的地址)存到棧頂中
            main.go:7       0x948325        4889442428              mov qword ptr [rsp+0x28], rax
            # 把1放入slice中
            main.go:7       0x94832a        48c70001000000          mov qword ptr [rax], 0x1
            # 從棧頂將slice的地址取出放入rcx寄存器中
            main.go:7       0x948331        488b4c2428              mov rcx, qword ptr [rsp+0x28]
            main.go:7       0x948336        8401                    test byte ptr [rcx], al
            # 把2放入slice中
            main.go:7       0x948338        48c7410802000000        mov qword ptr [rcx+0x8], 0x2
            main.go:7       0x948340        488b4c2428              mov rcx, qword ptr [rsp+0x28]
            main.go:7       0x948345        8401                    test byte ptr [rcx], al
            # 把3放入slice中
            main.go:7       0x948347        48c7411003000000        mov qword ptr [rcx+0x10], 0x3
            main.go:7       0x94834f        488b4c2428              mov rcx, qword ptr [rsp+0x28]
            main.go:7       0x948354        8401                    test byte ptr [rcx], al
            main.go:7       0x948356        eb00                    jmp 0x948358
            # 最後設置slice的指針,並將len和cap都設置為3
            main.go:7       0x948358        48894c2440              mov qword ptr [rsp+0x40], rcx
            main.go:7       0x94835d        48c744244803000000      mov qword ptr [rsp+0x48], 0x3
            main.go:7       0x948366        48c744245003000000      mov qword ptr [rsp+0x50], 0x3
    

    由此可見,使用字面量創建slice時,len和cap都會設置為初始化數據的個數。

    可以簡單看一下剛纔使用的runtime.newobject(),該函數在runtime/malloc.go文件中,代碼如下:

    func newobject(typ *_type) unsafe.Pointer {
        return mallocgc(typ.size, typ, true)
    }
    

    本質上還是通過記憶體管理機製為一個對象申請一塊連續空間並返回對應指針。

  2. make函數創建:

    // main.go
    package main
    
    import "fmt"
    
    func main() {
        s := make([]int, 10,20)
        fmt.Println(s)
    }
    

    該例子使用make方式創建了slice s,其len=10,cap=20,同樣使用delve進行debug,腳本同上,我們得到的反彙編結果如下:

    TEXT main.main(SB) D:/code/Notes/docs/go/list/main.go
            main.go:6       0xea8300        493b6610                cmp rsp, qword ptr [r14+0x10]
            main.go:6       0xea8304        0f86cb000000            jbe 0xea83d5
            main.go:6       0xea830a        4883ec70                sub rsp, 0x70
            main.go:6       0xea830e        48896c2468              mov qword ptr [rsp+0x68], rbp
            main.go:6       0xea8313        488d6c2468              lea rbp, ptr [rsp+0x68]
    =>      main.go:7       0xea8318*       488d0541840000          lea rax, ptr [rip+0x8441]
            main.go:7       0xea831f        bb0a000000              mov ebx, 0xa
            main.go:7       0xea8324        b914000000              mov ecx, 0x14
            # 調用runtime.makeslice函數創建slice(其實也只是創建了一個指針)
            main.go:7       0xea8329        e8b244faff              call $runtime.makeslice
            # 為創建好的對象設置起始指針,len和cap
            main.go:7       0xea832e        4889442438              mov qword ptr [rsp+0x38], rax
            main.go:7       0xea8333        48c74424400a000000      mov qword ptr [rsp+0x40], 0xa
            main.go:7       0xea833c        48c744244814000000      mov qword ptr [rsp+0x48], 0x14
    

    可以看到,這裡不再使用runtime.newobject()創建對象了,而是通過runtime.mallocslice()方法,該方法在runtime/slice.go文件中,源碼如下:

    func makeslice(et *_type, len, cap int) unsafe.Pointer {
        // 計算一下要申請的空間大小
        mem, overflow := math.MulUintptr(et.size, uintptr(cap))
        // 並判斷len和cap是否合理
        if overflow || mem > maxAlloc || len < 0 || len > cap {
            // NOTE: Produce a 'len out of range' error instead of a
            // 'cap out of range' error when someone does make([]T, bignumber).
            // 'cap out of range' is true too, but since the cap is only being
            // supplied implicitly, saying len is clearer.
            // See golang.org/issue/4085.
            mem, overflow := math.MulUintptr(et.size, uintptr(len))
            if overflow || mem > maxAlloc || len < 0 {
                panicmakeslicelen()
            }
            panicmakeslicecap()
        }
        // 最後還是要靠剛纔的方法申請空間返回指針
        return mallocgc(mem, et, true)
    }
    

    不過從這裡可以看到,slice底層物理空間的大小不是無限分配的,而是有上限的,其上限就是maxAlloc,該值的大小依賴於heapAddrBites,而heapAddrBites與操作系統有關。

    maxAlloc = (1 << heapAddrBits) - (1-_64bit)*1
    
  3. 切取其他數據結構

    // main.go
    package main
    
    import "fmt"
    func main() {
        s := [4]int{1,2,3,4}
        s3 := s[1:]
        fmt.Println(s3)
    }
    

    該例子通過數組s創建了其slice s3,並且內容為s的第2條和第三條數據,len=2。這裡反彙編一下,看一下結果:

            main.go:6       0xdc8318        488d05a1940000          lea rax, ptr [rip+0x94a1]
            main.go:6       0xdc831f        90                      nop
            # 為數組s分配空間
            main.go:6       0xdc8320        e81b53f6ff              call $runtime.newobject
            # 為數組s填充數據
            main.go:6       0xdc8325        4889442428              mov qword ptr [rsp+0x28], rax
            main.go:6       0xdc832a        48c70001000000          mov qword ptr [rax], 0x1
            main.go:6       0xdc8331        48c7400802000000        mov qword ptr [rax+0x8], 0x2
            main.go:6       0xdc8339        48c7401003000000        mov qword ptr [rax+0x10], 0x3
            main.go:6       0xdc8341        48c7401804000000        mov qword ptr [rax+0x18], 0x4
    =>      main.go:7       0xdc8349*       488b4c2428              mov rcx, qword ptr [rsp+0x28]
            main.go:7       0xdc834e        8401                    test byte ptr [rcx], al
            main.go:7       0xdc8350        eb00                    jmp 0xdc8352
            main.go:7       0xdc8352        eb00                    jmp 0xdc8354
            main.go:7       0xdc8354        488d5108                lea rdx, ptr [rcx+0x8]
            # 為切片設置起始地址以及len和cap
            main.go:7       0xdc8358        4889542440              mov qword ptr [rsp+0x40], rdx
            main.go:7       0xdc835d        48c744244802000000      mov qword ptr [rsp+0x48], 0x2
            main.go:7       0xdc8366        48c744245003000000      mov qword ptr [rsp+0x50], 0x3
    

    這個例子有兩個點要註意:

    1. slice s3的cap(容量)是3,也就是是原始容量減去slice起始值,這裡需要特別註意
    2. 這個例子中slice s3沒有被在記憶體中分配指針,而是在棧中分配的,這個點有待考察。
  4. 兩種聲明方式

    // main.go
    package main
    
    import "fmt"
    
    func main() {
        var s4 []int
        s5 := []int{}
        fmt.Println(s4)
        fmt.Println(s5)
    }
    

    該例子實現了對slice的兩種聲明方式,首先查看第7行於第9行的彙編代碼:

            # 將slice的起始地址設置為0
            main.go:7       0xe58326        48c744246800000000              mov qword ptr [rsp+0x68], 0x0
            main.go:7       0xe5832f        440f117c2470                    movups xmmword ptr [rsp+0x70], xmm15
            main.go:9       0xe58350        440f117c2440                    movups xmmword ptr [rsp+0x40], xmm15
            main.go:9       0xe58356        488d542440                      lea rdx, ptr [rsp+0x40]
            main.go:9       0xe5835b        4889542430                      mov qword ptr [rsp+0x30], rdx
            # 取出連續的三個空間
            main.go:9       0xe58360        488b442468                      mov rax, qword ptr [rsp+0x68]
            main.go:9       0xe58365        488b5c2470                      mov rbx, qword ptr [rsp+0x70]
            main.go:9       0xe5836a        488b4c2478                      mov rcx, qword ptr [rsp+0x78]
            # 將其轉化為slice再進行列印
            main.go:9       0xe5836f        e80c2af6ff                      call $runtime.convTslice
            main.go:9       0xe58374        4889442428                      mov qword ptr [rsp+0x28], rax
            main.go:9       0xe58379        488b7c2430                      mov rdi, qword ptr [rsp+0x30]
            main.go:9       0xe5837e        8407                            test byte ptr [rdi], al
            main.go:9       0xe58380        488d1599750000                  lea rdx, ptr [rip+0x7599]
            main.go:9       0xe58387        488917                          mov qword ptr [rdi], rdx
            main.go:9       0xe5838a        488d5708                        lea rdx, ptr [rdi+0x8]
            main.go:9       0xe5838e        833dbb610f0000                  cmp dword ptr [runtime.writeBarrier], 0x0
            main.go:9       0xe58395        7402                            jz 0xe58399
            main.go:9       0xe58397        eb06                            jmp 0xe5839f
            main.go:9       0xe58399        48894708                        mov qword ptr [rdi+0x8], rax
            main.go:9       0xe5839d        eb0a                            jmp 0xe583a9
            main.go:9       0xe5839f        4889d7                          mov rdi, rdx
            main.go:9       0xe583a2        e819ccfbff                      call $runtime.gcWriteBarrier
            main.go:9       0xe583a7        eb00                            jmp 0xe583a9
            main.go:9       0xe583a9        488b442430                      mov rax, qword ptr [rsp+0x30]
            main.go:9       0xe583ae        8400                            test byte ptr [rax], al
            main.go:9       0xe583b0        eb00                            jmp 0xe583b2
            main.go:9       0xe583b2        4889842498000000                mov qword ptr [rsp+0x98], rax
            main.go:9       0xe583ba        48c78424a000000001000000        mov qword ptr [rsp+0xa0], 0x1
            main.go:9       0xe583c6        48c78424a800000001000000        mov qword ptr [rsp+0xa8], 0x1
            main.go:9       0xe583d2        bb01000000                      mov ebx, 0x1
            main.go:9       0xe583d7        4889d9                          mov rcx, rbx
            main.go:9       0xe583da        e801abffff                      call $fmt.Println
    

    對於var s4 []int此類聲明,go僅僅是給該對象設置了一個nil指針,真正使用的時候,將其通過runtime.convTslice()轉化為slice,再使用。runtime.convTslice()源碼如下:

    func convTslice(val []byte) (x unsafe.Pointer) {
        // Note: this must work for any element type, not just byte.
        // 判斷起始指針是否為nil,是則返回一個空slice
        if (*slice)(unsafe.Pointer(&val)).array == nil {
            x = unsafe.Pointer(&zeroVal[0])
        } else {
            // 否則將記憶體中的數據給一個新的地址存儲
            x = mallocgc(unsafe.Sizeof(val), sliceType, true)
            *(*[]byte)(x) = val
        }
        return
    }
    

    這裡有一個點需要註意,convTslice()方法中入參val是一個struct slice{},由此,我們就可以追溯到slice的數據結構是如下模樣的:

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

    而對於s5 := []int{}這種聲明方式,其反彙編代碼如下:

            # 通過runtime.zerobase 返回一個預設0值
            main.go:8       0xe58335        488d15a4610f00                  lea rdx, ptr [runtime.zerobase]
            main.go:8       0xe5833c        4889542438                      mov qword ptr [rsp+0x38], rdx
            main.go:8       0xe58341        8402                            test byte ptr [rdx], al
            main.go:8       0xe58343        eb00                            jmp 0xe58345
            # 將其寫到s5的位置上
            main.go:8       0xe58345        4889542450                      mov qword ptr [rsp+0x50], rdx
            main.go:8       0xe5834a        440f117c2458                    movups xmmword ptr [rsp+0x58], xmm15
    

    使用s5時也是需要通過runtime.convTslice()將記憶體空間中的數據轉化為一個slice。可以看到這兩種方式都沒有真正的分配一塊記憶體,而是只寫了一個對象的指針,對於len和cap都沒有進行初始化,否則應該有連續三個8位元組的塊被初始化

總結一下,通過上面的分析,我們知道針對於5種創建slice的方式,其內部實現邏輯如下:

  1. 字面量創建:s := []int{1,2,3},該種方式會調用runtime.newobject實例化一個cap為提供數據個數的連續記憶體塊用於存放數據,本例中為3,創建的slice對象。
    1. 起始指針指向新創建的記憶體塊
    2. len = len(s)
    3. cap = len
  2. 內置函數make創建:make([]T, len, cap),該方式會通過runtime.makeslice創建一個大小為cap的記憶體塊,然後按照給定的參數將數據寫入slice中:
    1. 起始指針指向新創建的記憶體塊
    2. len = 給定的len
    3. cap = 給定的cap
  3. 切取其他數據結構:s := array[1:2],該方式不會再申請物理記憶體,而只是創建slice,並修改其值
    1. 起始指針指向被引用數組的被引用起始位置,本例中為array[1]的地址
    2. len = s中顯示指定的長度
    3. cap = 從被引用起始位置到被引用記憶體塊結束的位置的數據個數
  4. var s []int:賦值nil,使用時轉化為slice
  5. s := []int{}:賦值為nil,使用時轉化為slice

2. 隨機訪問

slice本身支持數據的隨機訪問,電腦基礎知識告訴我們,底層是通過計算目標數據的地址直接訪問的,這裡我們做實驗驗證一下,查看如下代碼:

// main.go
package main

import "fmt"

func main() {
	s := []int{1,2,3,4,5}
	s[2] = 10
	s[10] = 9
	fmt.Println(s)
}

上面的例子創建了一個slice s,並對其第3和第10個元素進行訪問,明顯前者是正確訪問的,後者會導致程式崩潰,我們通過反彙編查看該過程。

        main.go:7       0x418318        488d0561950000          lea rax, ptr [rip+0x9561]
        main.go:7       0x41831f        90                      nop
        # 創建slice底層數組,併為其填充數據
        main.go:7       0x418320        e81b53f6ff              call $runtime.newobject
        main.go:7       0x418325        4889442428              mov qword ptr [rsp+0x28], rax
        main.go:7       0x41832a        48c70001000000          mov qword ptr [rax], 0x1
        main.go:7       0x418331        488b4c2428              mov rcx, qword ptr [rsp+0x28]
        main.go:7       0x418336        8401                    test byte ptr [rcx], al
        main.go:7       0x418338        48c7410802000000        mov qword ptr [rcx+0x8], 0x2
        main.go:7       0x418340        488b4c2428              mov rcx, qword ptr [rsp+0x28]
        main.go:7       0x418345        8401                    test byte ptr [rcx], al
        main.go:7       0x418347        48c7411003000000        mov qword ptr [rcx+0x10], 0x3
        main.go:7       0x41834f        488b4c2428              mov rcx, qword ptr [rsp+0x28]
        main.go:7       0x418354        8401                    test byte ptr [rcx], al
        main.go:7       0x418356        48c7411804000000        mov qword ptr [rcx+0x18], 0x4
        main.go:7       0x41835e        488b4c2428              mov rcx, qword ptr [rsp+0x28]
        main.go:7       0x418363        8401                    test byte ptr [rcx], al
        main.go:7       0x418365        48c7412005000000        mov qword ptr [rcx+0x20], 0x5
        main.go:7       0x41836d        488b4c2428              mov rcx, qword ptr [rsp+0x28]
        main.go:7       0x418372        8401                    test byte ptr [rcx], al
        main.go:7       0x418374        eb00                    jmp 0x418376
        # 創建slice對象
        main.go:7       0x418376        48894c2440              mov qword ptr [rsp+0x40], rcx
        main.go:7       0x41837b        48c744244805000000      mov qword ptr [rsp+0x48], 0x5
        main.go:7       0x418384        48c744245005000000      mov qword ptr [rsp+0x50], 0x5
        main.go:8       0x41838d        eb00                    jmp 0x41838f
        # 通過計算s[2]的地址訪問s[2]的值
        main.go:8       0x41838f        48c741100a000000        mov qword ptr [rcx+0x10], 0xa
=>      main.go:9       0x418397*       488b4c2448              mov rcx, qword ptr [rsp+0x48]
        main.go:9       0x41839c        488b542440              mov rdx, qword ptr [rsp+0x40]
        # 使用索引與s的len比較
        main.go:9       0x4183a1        4883f90a                cmp rcx, 0xa
        # 如果沒有問題,則將數據放入slice
        main.go:9       0x4183a5        7705                    jnbe 0x4183ac
        # 索引越界,程式處理出錯,跳轉到錯誤處理
        main.go:9       0x4183a7        e998000000              jmp 0x418444
        main.go:9       0x4183ac        48c7425009000000        mov qword ptr [rdx+0x50], 0x9

通過這部分反彙編代碼,我們就可以清楚地看到隨機訪問的整個過程。

3. 追加

slice本身是可以進行修改的,go提供了append([]T, T...)方法用於歲slice進行數據追加,同時也通過該方法實現了slice的擴容,接下來我們通過下麵的例子對slice的追加策略進行探究。

// main.go
package main

import "fmt"

func main() {
	s := []int{}
	s = append(s, 1)
	s = append(s, 2)
	s = append(s, 3)
	fmt.Println(s)
}

這裡為了通用性,我們分析第9行代碼s = append(s, 2),因為第8行s = append(s, 1)必定需要擴容,所以不能代表全部情況,現在查看第9行反彙編代碼:

        main.go:9       0xa58378        488d7302                        lea rsi, ptr [rbx+0x2]
        main.go:9       0xa5837c        0f1f4000                        nop dword ptr [rax], eax
        # 比較slice當前cap和需要的容量
        main.go:9       0xa58380        4839f1                          cmp rcx, rsi
        # 如果當前容量夠用,直接插入數據
        main.go:9       0xa58383        7302                            jnb 0xa58387
        # 如果當前容量不夠用,進行擴容
        main.go:9       0xa58385        eb02                            jmp 0xa58389
        main.go:9       0xa58387        eb27                            jmp 0xa583b0
        # 擴容代碼
        main.go:8       0xa58389        48895c2440                      mov qword ptr [rsp+0x40], rbx
        main.go:9       0xa5838e        4889c3                          mov rbx, rax
        main.go:9       0xa58391        4889cf                          mov rdi, rcx
        main.go:9       0xa58394        488d05c5830000                  lea rax, ptr [rip+0x83c5]
        main.go:9       0xa5839b        4889d1                          mov rcx, rdx
        main.go:9       0xa5839e        6690                            data16 nop
        main.go:9       0xa583a0        e87b45faff                      call $runtime.growslice
        main.go:9       0xa583a5        488d7301                        lea rsi, ptr [rbx+0x1]
        main.go:9       0xa583a9        488b5c2440                      mov rbx, qword ptr [rsp+0x40]
        main.go:9       0xa583ae        eb00                            jmp 0xa583b0
        # 插入新數據代碼
        main.go:9       0xa583b0        488d14d8                        lea rdx, ptr [rax+rbx*8]
        main.go:9       0xa583b4        488d5208                        lea rdx, ptr [rdx+0x8]
        # s = append(s,T),將新的slice放回到原地址中
        main.go:9       0xa583b8        48c70202000000                  mov qword ptr [rdx], 0x2
        main.go:9       0xa583bf        4889442470                      mov qword ptr [rsp+0x70], rax
        main.go:9       0xa583c4        4889742478                      mov qword ptr [rsp+0x78], rsi
        main.go:9       0xa583c9        48898c2480000000                mov qword ptr [rsp+0x80], rcx

可以看到,go語言中slice是否需要擴容的判斷並不是在go中實現的,而擴容的具體邏輯是runtime.growslice()函數。下麵查看runtime.growslice()源碼:

// runtime/slice.go

// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
	...
    
	if cap < old.cap {
		panic(errorString("growslice: cap out of range"))
	}
    ...

	newcap := old.cap    
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
        // 1. 計算新slice的容量
		// (1) 如果原容量小於256,則是原容量的2倍
        const threshold = 256
		if old.cap < threshold {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
                // (2)否則每次增加 old.cap/4 + 192
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

	var overflow bool
	var lenmem, newlenmem, capmem uintptr
    // 2. 根據新容量快速算出是否需要多少記憶體
	// Specialize for common values of et.size.
	// For 1 we don't need any division/multiplication.
	// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
	// For powers of 2, use a variable shift.
	switch {
	case et.size == 1:
		lenmem = uintptr(old.len)
		newlenmem = uintptr(cap)
		capmem = roundupsize(uintptr(newcap))
		overflow = uintptr(newcap) > maxAlloc
		newcap = int(capmem)
	case et.size == goarch.PtrSize:
		lenmem = uintptr(old.len) * goarch.PtrSize
		newlenmem = uintptr(cap) * goarch.PtrSize
		capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
		newcap = int(capmem / goarch.PtrSize)
	case isPowerOfTwo(et.size):
		var shift uintptr
		if goarch.PtrSize == 8 {
			// Mask shift for better code generation.
			shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
		} else {
			shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
		}
		lenmem = uintptr(old.len) << shift
		newlenmem = uintptr(cap) << shift
		capmem = roundupsize(uintptr(newcap) << shift)
		overflow = uintptr(newcap) > (maxAlloc >> shift)
		newcap = int(capmem >> shift)
	default:
		lenmem = uintptr(old.len) * et.size
		newlenmem = uintptr(cap) * et.size
		capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
		capmem = roundupsize(capmem)
		newcap = int(capmem / et.size)
	}

	// The check of overflow in addition to capmem > maxAlloc is needed
	// to prevent an overflow which can be used to trigger a segfault
	// on 32bit architectures with this example program:
	//
	// type T [1<<27 + 1]int64
	//
	// var d T
	// var s []T
	//
	// func main() {
	//   s = append(s, d, d, d, d)
	//   print(len(s), "\n")
	// }
	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: cap out of range"))
	}

	var p unsafe.Pointer
	if et.ptrdata == 0 {
		p = mallocgc(capmem, nil, false)
		// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
		// Only clear the part that will not be overwritten.
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
	} else {
        // 3. 分配新slice的物理空間
		// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
		p = mallocgc(capmem, et, true)
		if lenmem > 0 && writeBarrier.enabled {
			// Only shade the pointers in old.array since we know the destination slice p
			// only contains nil pointers because it has been cleared during alloc.
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
		}
	}
    // 4. 將舊數據拷貝到新空間中
	memmove(p, old.array, lenmem)
    // 5. 返回生成的slice
	return slice{p, old.len, newcap}
}

通過上面的分析,我們可以看到,slice的擴容策略是:

  1. 舊slice容量 < 256,則newSlice.cap兩倍遞增
  2. 舊slice容量 >= 256,則newSlice.cap = (old.cap + 3 * 256)/4

而且註意,這裡產生了一個新的指針,新指針與舊指針指向的位置不同,因此才需要s = append(s, T)。

至此,slice的內容基本分析完畢。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 問題描述:最近工作中出現一個需求,純前端下載 Excel 數據,並且有的下載內容很多,這時需要給下載增加一個 loading 效果。 代碼如下: // utils.js const XLSX = require('xlsx') // 將一 ...
  • # React Native 封裝Toast ## 前言 > 使用react native的小伙伴都知道,官方並未提供輕提示組件,只提供了ToastAndroid API,顧名思義,只能再安卓環境下使用,對於ios就愛莫能助,故此,只能通過官方的核心組件,自行封裝,實現Toast功能 ## 實現 * ...
  • 本文主要介紹 AI 繪圖開源工具 Stable Diffusion WebUI 的 API 開啟和基本調用方法,通過本文的閱讀,你將瞭解到 stable-diffusion-webui 的基本介紹、安裝及 API 環境配置;文生圖、圖生圖、局部重繪、後期處理等 API 介面調用;圖像處理開發中常用到... ...
  • 今天給大家介紹下掃碼登錄功能是怎麼設計的。 掃碼登錄功能主要分為三個階段:待掃描、已掃描待確認、已確認。 整體流程圖如圖。 下麵分階段來看看設計原理。 1、待掃描階段 首先是待掃描階段,這個階段是 PC 端跟服務端的交互過程。 每次用戶打開PC端登陸請求,系統返回一個唯一的二維碼ID,並將二維碼ID ...
  • 前言 前面說了很多Kafka的性能優點,有些童鞋要說了,這Kafka在企業開發或者企業級應用中要怎麼用呢?今天咱們就來簡單探究一下。 1、 使用 Kafka 進行消息的非同步處理 Kafka 提供了一個可靠的消息傳遞機制,使得企業能夠將不同組件之間的通信解耦,實現高效的非同步處理。在企業級應用中,可以通 ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 題目描述 - 難度:中等 - 給定一個不含重覆數字的數 ...
  • 這些是一些常見的 Linux 命令,用於處理文件、進程、網路、用戶、系統管理等各種任務。根據你的需要,你可能會經常使用其中一些命令來管理和操作你的 Linux 系統。每個命令都有更多的選項和用法,你可以通過在終端中運行 man 命令名 來查看命令的詳細幫助文檔。 ...
  • 1. Python中常用的庫有哪些,作用分別是什麼 requests: requests 是一個用於發送 HTTP 請求的庫,它提供了簡單而優雅的 API,可以輕鬆地發送 GET、POST、PUT、DELETE 等請求,並處理響應數據。它支持會話管理、身份驗證、文件上傳等常見的 HTTP 功能,是進 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...