首頁 > 後端開發 > Golang > Go 中的陣列與切片:以視覺方式理解「底層」功能

Go 中的陣列與切片:以視覺方式理解「底層」功能

Patricia Arquette
發布: 2024-12-21 18:27:15
原創
274 人瀏覽過

Arrays vs Slices in Go: Understanding the

您是否曾經嘗試過收拾行李去旅行而不知道要在那裡待多久?這正是我們在 Go 中儲存資料時發生的情況。有時,例如週末旅行收拾行李時,我們確切地知道需要存放多少東西;其他時候,例如,在收拾行李準備旅行時,我們會說“我準備好後就回來”,但我們不會。

讓我們深入了解 Go 陣列的世界,並透過簡單的插圖剖析內部結構。我們將調查:

  1. 記憶體佈局
  2. 成長機制
  3. 參考語意
  4. 性能影響

讀完本文後,您將能夠藉助現實範例和記憶體圖了解何時使用陣列以及何時使用切片

陣列:固定大小的容器?

將陣列視為單一記憶體區塊,其中每個元素彼此相鄰,就像一排完美排列的盒子。

當你宣告 var number [5]int 時,Go 會預留足夠的連續記憶體來容納 5 個整數,不多也不少。

Arrays vs Slices in Go: Understanding the

由於它們具有連續的固定內存,因此無法在運行時調整大小。

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
}
登入後複製
登入後複製
登入後複製

Arrays vs Slices in Go: Understanding the

大小是陣列類型的一部份。這意味著 [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 中指派或傳遞陣列時,它們預設會建立副本。這確保了數據隔離並防止意外突變。

Arrays vs Slices in Go: Understanding the

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 中的每個切片都包含三個關鍵組件:

Arrays vs Slices in Go: Understanding the

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 因為:

  1. 它需要指向沒有型別資訊的原始記憶體
  2. 它允許 Go 實作任何類型 T 的切片,而無需為每種類型產生單獨的程式碼。

切片的動態機制

讓我們試著培養對底層實際演算法的直覺。

Arrays vs Slices in Go: Understanding the

如果我們遵循直覺我們可以做兩件事:

  1. 我們可以留這麼大的空間,需要的時候就可以使用
    優點:一定程度上滿足不斷增長的需求
    缺點:記憶浪費,其實可能達到極限

  2. 我們可以最初設定一個隨機大小,當元素被附加時,我們可以在每個附加上重新分配記憶體
    優點:處理之前的情況,可以根據需要進行增長
    缺點:重新分配的成本很高,而且每次追加都會變得最糟

我們無法避免重新分配,因為當容量達到需要成長時。我們可以最小化重新分配,以便後續插入/追加成本保持不變 (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
}
登入後複製
登入後複製
登入後複製

由於將大切片加倍會浪費內存,因此隨著切片大小的增加,生長因子會減少。

讓我們從使用的角度更好地理解

Arrays vs Slices in Go: Understanding the

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
}
登入後複製
登入後複製

哎呀!現在我們已經達到了我們的能力,我們需要成長。發生的情況如下:

  1. 計算新容量(oldCap
  2. 分配新的後備數組(新的記憶體位址,例如 300)
  3. 將現有元素複製到新的支援數組
  4. 新增元素
  5. 更新切片標題

Arrays vs Slices in Go: Understanding the

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
}
登入後複製
登入後複製
登入後複製

為什麼要引用語意?

  1. 效能:複製大型資料結構的成本很高
  2. 記憶體效率:避免不必要的資料重複
  3. 啟用資料共享視圖:多個切片可以引用同一後備數組
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
登入後複製
登入後複製

slice的使用模式與注意事項

意外更新

由於切片使用引用語義,因此它不會建立副本,如果不注意,可能會導致原始切片意外突變。

// 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)
登入後複製

Arrays vs Slices in Go: Understanding the

讓我們用一個清晰的選擇指南來結束這個:

?在下列情況下選擇陣列:

  1. 您預先知道確切的尺寸
  2. 處理小型固定資料(如座標、RGB 值)
  3. 效能至關重要,資料適合堆疊
  4. 您想要型別安全與大小

?選擇切片的時間:

  1. 尺寸可能會改變
  2. 使用動態資料
  3. 需要相同資料的多個視圖
  4. 處理流/集合

?查看 notion-to-md 項目!它是一個將 Notion 頁面轉換為 Markdown 的工具,非常適合內容創作者和開發人員。加入我們的不和諧社群。

以上是Go 中的陣列與切片:以視覺方式理解「底層」功能的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板