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 的內部實現。
上圖表示 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]
新 slice 的指標變數的值與初始基礎陣列的索引位置 2 和 3 相關聯。就這個新 slice 而言,我們現在有一個包含 3 個元素的基礎陣列,我們只使用這 3 個元素中的 2 個。這個 slice 無法訪問初始底層陣列的前兩個元素。
執行 slice 操作時,第一個引數指定 slice 指標變數位置的起始索引。在我們的例子中,我們說索引 2 是初始底層陣列中的 3 個元素,我們從中獲取 slice。第二個引數是最後一個索引位置加一(+1)。在我們的例子中,我們說索引 4 將包括索引 2(起始位置)和索引 3(最終位置)之間的所有索引。
執行 slice 操作時,我們並不總是需要包含起始或結束索引位置:
newSlice2 = newSlice[:cap(newSlice)]
在此示例中,我們使用之前建立的新 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 函式驗證您的假設。