1. 程式人生 > 其它 >golang slice 記憶體洩露_理解 Go 程式設計中的 slice

golang slice 記憶體洩露_理解 Go 程式設計中的 slice

技術標籤:golang slice 記憶體洩露

自從我開始使用 Go 程式設計以來,slice 的概念和使用一直令人困惑。slice 看起來像一個數組,感覺就像一個數組,但它們不僅僅是一個數組,對我來說是一種全新的概念。我一直在閱讀 Go 程式設計師如何使用 slice,我認為現在終於明白了 slice 的用途。

Andrew Gerrand 撰寫了一篇非常棒的關於 slice 的博文:

http://blog.golang.org/go-slices-usage-and-internals

沒有理由重複 Andrew 所寫的一切,所以請在開始之前閱讀他的博文。這篇文章專注於 slice 的內部實現。

359c5d582ed6e5e8167dc106afb1dc49.png

3075ac6b26bb3447a3e6b95a4af8f879.png

上圖表示 slice 的內部結構。分配 slice 時,將建立此資料結構以及對應的基礎陣列。slice 變數的值指向該資料結構。將 slice 傳遞給函式時,會在堆疊上建立此資料結構的副本。

我們可以用兩種方式建立一個 slice:

這裡我們使用關鍵字 make 來建立 slice。需要傳遞我們儲存的資料型別,slice 的初始長度和底層陣列的容量,例如

mySlice := make([]string, 5, 8)
mySlice[0] = "Apple"
mySlice[1] = "Orange"
mySlice[2] = "Banana"
mySlice[3] = "Grape"
mySlice[4] = "Plum"

// You don ’ t need to include the capacity. Length and Capacity will be the same
mySlice := make([]string, 5)

您還可以使用 slice 字面量來定義 slice。在這種情況下,長度和容量將是相同的。請注意,中括號[]內沒有提供任何值。如果新增一個值,您將擁有一個 array。如果您不新增值,您將獲得一個 slice。

mySlice := []string{"Apple", "Orange", "Banana", "Grape", "Plum"}

建立 slice 後,無法擴充套件 slice 的容量。更改容量的唯一方法是建立新 slice 並執行復制。Andrew 提供了一個很好的示例函式,它顯示了檢查剩餘容量的有效方法,並且只在必要時執行復制。

slice 的長度標識了我們從起始索引位置使用的基礎陣列的元素數量。容量標識了我們可以使用的總元素數量。

我們可以從原始 slice 建立一個新 slice:

newSlice := mySlice[2:4]

9ce483b8606c069640fe1b8c13b8e5ff.png

新 slice 的指標變數的值與初始基礎陣列的索引位置 2 和 3 相關聯。就這個新 slice 而言,我們現在有一個包含 3 個元素的基礎陣列,我們只使用這 3 個元素中的 2 個。這個 slice 無法訪問初始底層陣列的前兩個元素。

執行 slice 操作時,第一個引數指定 slice 指標變數位置的起始索引。在我們的例子中,我們說索引 2 是初始底層陣列中的 3 個元素,我們從中獲取 slice。第二個引數是最後一個索引位置加一(+1)。在我們的例子中,我們說索引 4 將包括索引 2(起始位置)和索引 3(最終位置)之間的所有索引。

執行 slice 操作時,我們並不總是需要包含起始或結束索引位置:

newSlice2 = newSlice[:cap(newSlice)]

00053aca5218a7bf75b897b645f93f7a.png

在此示例中,我們使用之前建立的新 slice 來建立第三個 slice。我們不提供起始索引位置,但我們確實指定了最後一個索引位置。我們最新的 slice 具有相同的起始位置和容量,但長度已經改變。通過將最後一個索引位置指定為容量大小,此 slice 的此長度使用基礎陣列中的所有剩餘元素。

現在讓我們執行一些程式碼來證明這個資料結構實際存在,並且 slice 按照說明的方式工作。

我建立了一個函式來檢查與任何 slice 關聯的記憶體:

func InspectSlice(slice []string) {
    // Capture the address to the slice structure
    address := unsafe.Pointer(&slice)
    addrSize := unsafe.Sizeof(address)

    // Capture the address where the length and cap size is stored
    lenAddr := uintptr(address) + addrSize
    capAddr := uintptr(address) + (addrSize * 2)

    // Create pointers to the length and cap size
    lenPtr := (*int)(unsafe.Pointer(lenAddr))
    capPtr := (*int)(unsafe.Pointer(capAddr))

    // Create a pointer to the underlying array
    addPtr := (*[8]string)(unsafe.Pointer(*(*uintptr)(address)))

    fmt.Printf("Slice Addr[%p] Len Addr[0x%x] Cap Addr[0x%x]n",
        address,
        lenAddr,
        capAddr)

    fmt.Printf("Slice Length[%d] Cap[%d]n",
        *lenPtr,
        *capPtr)

    for index := 0; index < *lenPtr; index++ {
        fmt.Printf("[%d] %p %sn",
            index,
            &(*addPtr)[index],
            (*addPtr)[index])
    }

    fmt.Printf("nn")
}

此函式正在執行一系列指標操作,因此我們可以檢查 slice 的資料結構和底層陣列的記憶體和值。

我們將它分解,但首先讓我們建立一個 slice 並通過 inspect 函式執行它:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    orgSlice := make([]string, 5, 8)
    orgSlice[0] = "Apple"
    orgSlice[1] = "Orange"
    orgSlice[2] = "Banana"
    orgSlice[3] = "Grape"
    orgSlice[4] = "Plum"

    InspectSlice(orgSlice)
}

這是程式的輸出:

Slice Addr[0x2101be000] Len Addr[0x2101be008] Cap Addr[0x2101be010]
Slice Length[5] Cap[8]
[0] 0x2101bd000 Apple
[1] 0x2101bd010 Orange
[2] 0x2101bd020 Banana
[3] 0x2101bd030 Grape
[4] 0x2101bd040 Plum

正如 Andrew 所描述的那樣,slice 的資料結構確實存在。

InspectSlice 函式首先顯示 slice 資料結構的地址以及長度和容量值應該在的地址位置。然後通過使用這些地址建立 int 指標,我們顯示長度和容量的值。最後,我們建立一個指向底層陣列的指標。使用指標,我們遍歷底層陣列,顯示索引位置,元素的起始地址和值。

讓我們分解 InspectSlice 函式來理解它是如何工作的:

// Capture the address to the slice structure
address := unsafe.Pointer(&slice)
addrSize := unsafe.Sizeof(address)

// Capture the address where the length and cap size is stored
lenAddr := uintptr(address) + addrSize
capAddr := uintptr(address) + (addrSize * 2)

unsafe.Pointer 是一種對映到 uintptr 型別的特殊型別。因為我們需要執行指標運算,所以我們需要使用通用指標。第一行程式碼將 slice 的資料結構的地址強制轉換為 unsafe.Pointer。然後我們得到編碼執行的架構的地址大小。現在知道了地址大小,我們建立了兩個通用指標,分別將地址大小和地址大小位元組的兩倍指向 slice 的資料結構。

下圖顯示了每個指標變數,變數的值以及指標指向的值:

ble data -draft-n ode="block" data-draft-type="table" data-size="normal" data-row-style="normal">

有了我們的指標,我們現在可以建立正確的型別指標,以便我們可以顯示值。這裡我們建立兩個整數指標,可用於顯示 slice 資料結構的長度和容量值。

// Create pointers to the length and cap size
lenPtr := (*int)(unsafe.Pointer(lenAddr))
capPtr := (*int)(unsafe.Pointer(capAddr))

我們現在需要一個型別為[8]字串的指標,它是底層陣列的型別。

// Create a pointer to the underlying array
addPtr := (*[8]string)(unsafe.Pointer(*(*uintptr)(address)))

在這一個語句中有很多內容,所以讓我們將其分解:

(*uintptr)(address):0x2101be000。此程式碼獲取 slice 資料結構的起始地址並將其轉換為通用指標。

*(*uintptr)(address):0x2101bd000。然後我們得到指標指向的值,這是底層陣列的起始地址。

unsafe.Pointer(*(*uintptr)(address))。然後我們將底層陣列的起始地址轉換為 unsafe.Pointer 型別。我們需要一個這種型別的指標來執行最後的步驟。

(*[8]string)(unsafe.Pointer(*(*uintptr)(address))) 最後,我們將 unsafe.Pointer 轉換為正確型別的指標。

其餘程式碼使用正確的指標來顯示輸出:

fmt.Printf("Slice Addr[%p] Len Addr[0x%x] Cap Addr[0x%x]n",
    address,
    lenAddr,
    capAddr)

fmt.Printf("Slice Length[%d] Cap[%d]n",
    *lenPtr,
    *capPtr)

for index := 0; index < *lenPtr; index++ {
    fmt.Printf("[%d] %p %sn",
        index,
        &(*addPtr)[index],
        (*addPtr)[index])
}

現在讓我們將整個程式放在一起並建立一些 slice。我們將檢查每個 slice 並確保我們所知道的 slice 是真的:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    orgSlice := make([]string, 5, 8)
    orgSlice[0] = "Apple"
    orgSlice[1] = "Orange"
    orgSlice[2] = "Banana"
    orgSlice[3] = "Grape"
    orgSlice[4] = "Plum"

    InspectSlice(orgSlice)

    slice2 := orgSlice[2:4]
    InspectSlice(slice2)

    slice3 := slice2[1:cap(slice2)]
    InspectSlice(slice3)

    slice3[0] = "CHANGED"
    InspectSlice(slice3)
    InspectSlice(slice2)
}

func InspectSlice(slice []string) {
    // Capture the address to the slice structure
    address := unsafe.Pointer(&slice)
    addrSize := unsafe.Sizeof(address)

    // Capture the address where the length and cap size is stored
    lenAddr := uintptr(address) + addrSize
    capAddr := uintptr(address) + (addrSize * 2)

    // Create pointers to the length and cap size
    lenPtr := (*int)(unsafe.Pointer(lenAddr))
    capPtr := (*int)(unsafe.Pointer(capAddr))

    // Create a pointer to the underlying array
    addPtr := (*[8]string)(unsafe.Pointer(*(*uintptr)(address)))

    fmt.Printf("Slice Addr[%p] Len Addr[0x%x] Cap Addr[0x%x]n",
        address,
        lenAddr,
        capAddr)

    fmt.Printf("Slice Length[%d] Cap[%d]n",
        *lenPtr,
        *capPtr)

    for index := 0; index < *lenPtr; index++ {
        fmt.Printf("[%d] %p %sn",
            index,
            &(*addPtr)[index],
            (*addPtr)[index])
    }

    fmt.Printf("nn")
}

下面是每個 slice 的程式碼和輸出:

這裡我們建立一個初始 slice,其長度為 5 個元素,容量為 8 個元素。

Code:
orgSlice := make([]string, 5, 8)
orgSlice[0] = "Apple"
orgSlice[1] = "Orange"
orgSlice[2] = "Banana"
orgSlice[3] = "Grape"
orgSlice[4] = "Plum"

Output:
Slice Addr[0x2101be000] Len Addr[0x2101be008] Cap Addr[0x2101be010]
Slice Length[5] Cap[8]
[0] 0x2101bd000 Apple
[1] 0x2101bd010 Orange
[2] 0x2101bd020 Banana
[3] 0x2101bd030 Grape
[4] 0x2101bd040 Plum

輸出符合預期。長度為 5,容量為 8,底層陣列包含我們的值。

接下來,我們從原始 slice 中獲取 slice。我們要求索引 2 和 3 之間有 2 個元素。

Code:
slice2 := orgSlice[2:4]
InspectSlice(slice2)

Output:
Slice Addr[0x2101be060] Len Addr[0x2101be068] Cap Addr[0x2101be070]
Slice Length[2] Cap[6]
[0] 0x2101bd020 Banana
[1] 0x2101bd030 Grape

在輸出中,您可以看到我們有一個長度為 2 且容量為 6 的 slice。因為這個新 slice 在原始 slice 的底層陣列中啟動了 3 個元素,所以有 6 個元素的容量。容量包括新 slice 可以訪問的所有可能元素。新 slice 的索引 0 對映到原始 slice 的索引 2。它們都具有相同的地址 0x2101bd020。

這次我們要求從索引位置 1 開始 slice 到 slice2 的最後一個元素。

Code:
slice3 := slice2[1:cap(slice2)]
InspectSlice(slice3)

Output:
Slice Addr[0x2101be0a0] Len Addr[0x2101be0a8] Cap Addr[0x2101be0b0]
Slice Length[5] Cap[5]
[0] 0x2101bd030 Grape
[1] 0x2101bd040 Plum
[2] 0x2101bd050
[3] 0x2101bd060
[4] 0x2101bd070

正如預期的那樣,長度和容量都是 5. 當我們顯示 slice 的所有值時,您會看到最後三個元素沒有值。在建立基礎陣列時,slice 初始化了所有元素。此 slice 的索引 0 也對映到 slice2 的索引 1 和原始 slice 的索引 3。它們都具有相同的地址 0x2101bd030。

最終程式碼將第一個元素的值(slice3 中的索引 0)更改為值 CHANGED。然後我們顯示 slice3 和 slice2 的值。

slice3[0] = "CHANGED"
InspectSlice(slice3)
InspectSlice(slice2)

Slice Addr[0x2101be0e0] Len Addr[0x2101be0e8] Cap Addr[0x2101be0f0]
Slice Length[5] Cap[5]
[0] 0x2101bd030 CHANGED
[1] 0x2101bd040 Plum
[2] 0x2101bd050
[3] 0x2101bd060
[4] 0x2101bd070


Slice Addr[0x2101be120] Len Addr[0x2101be128] Cap Addr[0x2101be130]
Slice Length[2] Cap[6]
[0] 0x2101bd020 Banana
[1] 0x2101bd030 CHANGED

請注意,兩個 slice 都在其尊重索引中顯示更改的值。這證明了所有 slice 都使用相同的底層陣列。

InspectSlice 函式證明每個 slice 都包含自己的資料結構,其中包含指向底層陣列的指標,slice 的長度和容量。花些時間建立更多 slice 並使用 InspectSlice 函式驗證您的假設。