陣列基礎#

陣列是固定長度的同型別元素序列,長度是型別的一部分

// 陣列宣告
var arr1 [5]int                    // 零值初始化
arr2 := [5]int{1, 2, 3, 4, 5}      // 完整初始化
arr3 := [...]int{1, 2, 3}          // 自動推斷長度
arr4 := [5]int{0: 1, 4: 5}         // 指定索引初始化

[3]int[5]int不同的型別。陣列長度是編譯時期常數,無法在執行時期改變。

切片本質#

切片是對底層陣列的一個「視窗」,包含三個欄位:

type slice struct {
    array unsafe.Pointer  // 指向底層陣列的指標
    len   int             // 切片長度
    cap   int             // 切片容量
}
// 切片建立方式
s1 := make([]int, 5)        // 長度 5,容量 5
s2 := make([]int, 5, 10)    // 長度 5,容量 10
s3 := []int{1, 2, 3}        // 字面量初始化
s4 := arr[1:4]              // 從陣列切取

切片視窗比喻#

把切片想像成一個可以移動、伸縮的視窗,視窗後面是底層陣列:

arr := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
s := arr[2:6]  // s = [2, 3, 4, 5]

// 底層陣列:[0, 1, 2, 3, 4, 5, 6, 7]
//                  ↑──視窗──↑
// len(s) = 4(視窗寬度)
// cap(s) = 6(從起點到陣列末端的距離)

切片只能向右擴展(增加容量),無法向左擴展。一旦起點確定,容量上限就固定了。

切片擴容機制#

s := make([]int, 0, 4)
s = append(s, 1, 2, 3, 4)  // 長度 4,容量 4
s = append(s, 5)           // 觸發擴容

// 擴容規則(Go 1.18+):
// - 容量 < 256:新容量 = 舊容量 * 2
// - 容量 >= 256:新容量 = 舊容量 * 1.25 + 192(約略)

擴容會建立新的底層陣列並複製元素:

  1. 擴容操作有效能開銷
  2. 擴容後切片不再與原底層陣列共用記憶體
切片共用底層陣列的陷阱
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4]  // [2, 3, 4]
s2 := arr[2:5]  // [3, 4, 5]

// s1 和 s2 共用同一個底層陣列
s1[1] = 100     // 修改 s1
fmt.Println(s2) // [100, 4, 5] - s2 也被影響!

// 解法:用 copy 建立獨立切片
s3 := make([]int, len(s1))
copy(s3, s1)