您是否曾經嘗試過收拾行李去旅行而不知道要在那裡待多久?這正是我們在 Go 中儲存資料時發生的情況。有時,例如週末旅行收拾行李時,我們確切地知道需要存放多少東西;其他時候,例如,在收拾行李準備旅行時,我們會說“我準備好後就回來”,但我們不會。
讓我們深入了解 Go 陣列的世界,並透過簡單的插圖剖析內部結構。我們將調查:
讀完本文後,您將能夠藉助現實範例和記憶體圖了解何時使用陣列以及何時使用切片
將陣列視為單一記憶體區塊,其中每個元素彼此相鄰,就像一排完美排列的盒子。
當你宣告 var number [5]int 時,Go 會預留足夠的連續記憶體來容納 5 個整數,不多也不少。
由於它們具有連續的固定內存,因此無法在運行時調整大小。
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
大小是陣列類型的一部份。這意味著 [5]int 和 [6]int 是完全不同的類型,就像 int 和 string 不同一樣。
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
當你在 Go 中指派或傳遞陣列時,它們預設會建立副本。這確保了數據隔離並防止意外突變。
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
好吧,所以你不能使用 vardynamic [size]int 來設定動態大小,這就是 slice 發揮作用的地方。
神奇之處在於它如何在保持快速操作的同時保持這種靈活性。
Go 中的每個切片都包含三個關鍵組件:
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
什麼不安全。指針??
unsafe.Pointer 是 Go 在沒有型別安全約束的情況下處理原始記憶體位址的方式。它「不安全」因為它繞過了Go的型別系統,允許直接記憶體操作。
將其視為 Go 相當於 C 的 void 指標。
那個陣列是什麼?
當您建立切片時,Go 會在堆中分配一個連續的記憶體區塊(與陣列不同),稱為後備數組。現在切片結構中的陣列指向該記憶體區塊的開頭。
陣列欄位使用 unsafe.Pointer 因為:
讓我們試著培養對底層實際演算法的直覺。
如果我們遵循直覺我們可以做兩件事:
我們可以留這麼大的空間,需要的時候就可以使用
優點:一定程度上滿足不斷增長的需求
缺點:記憶浪費,其實可能達到極限
我們可以最初設定一個隨機大小,當元素被附加時,我們可以在每個附加上重新分配記憶體
優點:處理之前的情況,可以根據需要進行增長
缺點:重新分配的成本很高,而且每次追加都會變得最糟
我們無法避免重新分配,因為當容量達到需要成長時。我們可以最小化重新分配,以便後續插入/追加成本保持不變 (O(1))。這稱為攤餘成本。
我們該怎麼辦?
直到Go版本v1.17使用了以下公式:
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
來自 Go 版本 v1.18:
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
由於將大切片加倍會浪費內存,因此隨著切片大小的增加,生長因子會減少。
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
讓我們在切片上加入一些元素
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
由於我們有容量 (5) >長度 (3),請轉到:
使用現有的支援陣列
將 10 置於索引 3
長度增加 1
// Old growth pattern capacity = oldCapacity * 2 // Simple doubling
讓我們達到極限
// New growth pattern if capacity < 256 { capacity = capacity * 2 } else { capacity = capacity + capacity/4 // 25% growth }
哎呀!現在我們已經達到了我們的能力,我們需要成長。發生的情況如下:
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
如果是大片會怎麼樣?
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
由於容量為256,Go使用1.18後的成長公式:
新容量 = oldCap oldCap/4
256 256/4 = 256 64 = 320
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
這是切片標題的外觀:
// Old growth pattern capacity = oldCapacity * 2 // Simple doubling
意外更新
由於切片使用引用語義,因此它不會建立副本,如果不注意,可能會導致原始切片意外突變。
// New growth pattern if capacity < 256 { capacity = capacity * 2 } else { capacity = capacity + capacity/4 // 25% growth }
昂貴的追加操作
numbers := make([]int, 3, 5) // length=3 capacity // Memory Layout after creation: Slice Header: { array: 0xc0000b2000 // Example memory address len: 3 cap: 5 } Backing Array at 0xc0000b2000: [0|0|0|unused|unused]
複製與追加
numbers = append(numbers, 10)
讓我們用一個清晰的選擇指南來結束這個:
?在下列情況下選擇陣列:
?選擇切片的時間:
?查看 notion-to-md 項目!它是一個將 Notion 頁面轉換為 Markdown 的工具,非常適合內容創作者和開發人員。加入我們的不和諧社群。
以上是Go 中的陣列與切片:以視覺方式理解「底層」功能的詳細內容。更多資訊請關注PHP中文網其他相關文章!