揭祕 Go 切片(Slice)的祕密

當向切片添加新參數時,底層數組會發生什麼變化?它會擴展以容納更多元素嗎?

在這篇文章中,我們將深入探討切片的內部工作原理,以及如何利用這些知識來進行更好的內存管理和性能優化。

具體而言,我們將探索 Go 中切片的底層實現和內存管理機制。

讓我們開始吧!

查看數組

要深入瞭解切片的結構,必須仔細查看其底層類型:數組。

func main() {
    a := [5]int{}
    fmt.Printf("%p, %p\n"&a, &a[0])
}

// 0x14000018240, 0x14000018240

正如您可能已經瞭解的那樣,數組中第一個元素的內存位置也是數組本身的內存位置(這意味着當您將數組傳遞給函數或賦值給變量時,您實際上是傳遞或賦值了第一個元素的內存地址)。

因此,數組的內存佈局是連續的內存塊,每個元素依次放置在相鄰的位置上。

切片的結構

切片有三個主要組成部分:

這些關於切片結構的信息是從 Go 運行時庫中獲取的。現在,讓我們更詳細地瞭解一下。

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

出於演示目的,這裏有一個關於切片長度和容量概念的示例(如果你已經熟悉這些概念,可以忽略這個示例)。

func main() {
    original := []int{0, 1, 2, 3, 4}
    s := original[1:2] 
    fmt.Println(len(s), cap(s))
}

// 1 4

在這個示例中,我們可以看到切片s等於[]int{1},它的容量是從原始數組的索引 1 到索引 4 的部分計算得到的。

底層數組將會改變

需要注意的是,修改切片中的元素有時會影響到底層的數組,但並非總是如此,也不應該依賴這種行爲。

在某些情況下,底層的數組可能會發生改變,導致切片也發生改變。然而,在編寫代碼時不應該依賴這種行爲,因爲它可能會導致意想不到的結果。

func main() {
    original := []int{0, 1, 2, 3, 4}
    s := original[:]

    fmt.Println("Same array:")
    s[0] = 100
    fmt.Println(original, s)

    fmt.Println("Different array:")
    s = append(s, 5)
    s[0] = 200
    fmt.Println(original, s)
}

// Same array:
// [100 1 2 3 4] [100 1 2 3 4]
// Different array:
// [100 1 2 3 4] [200 1 2 3 4 5]

在實際情況中,append()函數不僅僅是用於添加元素。它還負責處理切片的分配和調整大小。

    1. 它會檢查切片是否有足夠的容量來存儲新的元素。
  1. 2. 如果容量不足,它會創建一個具有更大容量的新切片,複製原始切片的元素到新切片,並將新切片賦值給原始切片。

    1. 然後,它將新的元素添加到切片中。

這是我用更簡單的方式重寫的append()函數版本,利用了泛型:

func append[T any]([]T, x ...T) []{
    n := len(s)
    maxN := len(s) + len(x)

    // If there is not enough capacity, create a new slice with larger capacity
    if n+len(x) > cap(s) {
        newSlice := make([]T, maxN, maxN*2)
        copy(newSlice, s) // Copy the elements from the original slice to the new slice
        s = newSlice
    }

    s = s[:maxN]
    copy(s[n:], x)
    return s
}

預分配技術

重新調整切片大小在性能和內存方面可能非常昂貴,因爲它需要分配一個新的切片並將所有元素複製過去。

這就是爲什麼在使用切片時,如果我們可以預測它們將保存的元素數量,通常最好進行預分配。這有助於提高性能並防止不必要的內存分配。

“是否可以同時使用 “append()” 和預分配?使用索引賦值可能很麻煩”

是的,這是可能的。

你可以使用make()函數進行預分配切片,傳入兩個變量,一個用於長度,另一個用於容量,而不是隻傳入一個。這可以消除索引賦值的需要。

func main() {
    s := make([]int, 0, 3)
    s = append2(s, 1, 2, 3, 4)
    fmt.Println(s)
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/g_N2-NIbFTbfQyx9b7HrZg