本文 AI 產出,尚未審核

Golang 多維陣列與切片

簡介

在日常開發中,我們常常需要處理 矩陣、棋盤、影像資料 等二維或更高維度的結構。Go 語言提供了 陣列 (array)切片 (slice) 兩種基礎資料結構,配合語法糖與內建函式,即可輕鬆建立與操作多維資料。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握 多維陣列與切片 的使用技巧,讓你在實務專案中能快速、正確地處理複雜資料。

核心概念

1. 多維陣列的定義

在 Go 中,陣列的長度是編譯期固定的,多維陣列其實是「陣列的陣列」。例如:

// 宣告一個 3x4 的整數陣列
var matrix [3][4]int

此時 matrix 的類型是 [3][4]int,內部實際上是 3 個長度為 4 的一維陣列。索引時必須一次寫出所有維度:

matrix[0][2] = 7   // 設定第一列第三個元素為 7
fmt.Println(matrix[0][2]) // 輸出 7

為什麼陣列適合「小且固定」的資料?

  • 記憶體布局連續,存取速度快。
  • 編譯期檢查長度,能防止超出範圍的錯誤。
  • 但缺點是 無法動態調整大小,若需要彈性則應使用切片。

2. 多維切片的建立

切片本質上是 指向底層陣列的描述子,包含指標、長度與容量。多維切片則是 切片的切片,每一層都可以獨立調整長度與容量。

2.1 直接宣告

// 建立 3x4 的整數切片
rows, cols := 3, 4
grid := make([][]int, rows)        // 外層切片,長度為 rows
for i := range grid {
    grid[i] = make([]int, cols)    // 為每一列分配內層切片
}

2.2 使用字面值

// 直接以字面值寫出 2x3 的字串切片
words := [][]string{
    {"go", "java", "python"},
    {"ruby", "php", "csharp"},
}
fmt.Println(words[1][2]) // 輸出 csharp

2.3 轉換陣列為切片

如果已有固定長度的陣列,想要以切片方式操作:

var arr [2][3]float64 = [2][3]{{1.1, 2.2, 3.3}, {4.4, 5.5, 6.6}}
slice := arr[:][:] // 先切第一層,再切第二層
fmt.Println(slice[0][1]) // 2.2

Tiparr[:][:] 只是一種語法糖,實際上會先把外層陣列切成切片,再對內層做同樣的切片。


3. 迭代多維結構

使用 for 迴圈搭配 range 可以同時取得索引與值:

// 迭代多維切片,計算所有元素的總和
sum := 0
for i, row := range grid {
    for j, val := range row {
        fmt.Printf("grid[%d][%d] = %d\n", i, j, val)
        sum += val
    }
}
fmt.Println("總和:", sum)

對於多維陣列,range 仍然適用,只是取得的值是 固定長度的陣列

for i, row := range matrix {
    fmt.Printf("第 %d 列: %v\n", i, row) // row 型別是 [4]int
}

4. 常見操作:追加、刪除與複製

操作 陣列 切片
追加元素 不支援(長度固定) append(slice, val)
刪除元素 需自行搬移或重新宣告 slice = append(slice[:i], slice[i+1:]...)
深拷貝 直接賦值會複製整個陣列 必須自行迭代或使用 copy(淺層拷貝)
// 切片追加範例:在第 2 列的第 3 個位置插入 99
grid[1] = append(grid[1][:2], append([]int{99}, grid[1][2:]...)...)
fmt.Println(grid[1]) // 輸出 [0 0 99 0 0 0]

程式碼範例

範例 1:建立與填充 5x5 的棋盤(整數切片)

package main

import "fmt"

func main() {
    const size = 5
    board := make([][]int, size)
    for i := range board {
        board[i] = make([]int, size)
        for j := range board[i] {
            // 交錯填入 0 與 1,模擬棋盤格子
            board[i][j] = (i + j) % 2
        }
    }

    // 輸出棋盤
    for _, row := range board {
        fmt.Println(row)
    }
}

說明:使用 make 動態分配每一列,使得即使未來想改成 size = 8 也只需要改一個常數。


範例 2:將固定長度的 3x3 陣列轉為切片並旋轉 90 度

package main

import "fmt"

func rotate90(a [3][3]int) [][]int {
    // 先把陣列轉成切片
    s := make([][]int, 3)
    for i := range s {
        s[i] = make([]int, 3)
    }

    // 旋轉演算法:new[i][j] = old[2-j][i]
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            s[i][j] = a[2-j][i]
        }
    }
    return s
}

func main() {
    var m = [3][3]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    r := rotate90(m)
    for _, row := range r {
        fmt.Println(row)
    }
}

重點:此範例示範了 陣列 → 切片 的轉換與 演算法實作,適合需要在記憶體中重新排列資料的情境。


範例 3:使用多維切片儲存不規則矩陣(列長度不同)

package main

import "fmt"

func main() {
    // 每一列的長度可以不同
    jagged := [][]int{
        {1, 2, 3},
        {4, 5},
        {6, 7, 8, 9},
    }

    // 逐列印出元素
    for i, row := range jagged {
        fmt.Printf("第 %d 列 (%d 個元素): %v\n", i, len(row), row)
    }

    // 在第 2 列尾端追加元素 10
    jagged[1] = append(jagged[1], 10)
    fmt.Println("追加後:", jagged[1])
}

說明:多維切片天生支援「不規則」矩陣(jagged array),這在 圖形、稀疏矩陣 等情境非常有用。


範例 4:深拷貝 2D 切片(避免共用底層陣列)

package main

import "fmt"

func deepCopy(src [][]int) [][]int {
    dst := make([][]int, len(src))
    for i := range src {
        dst[i] = make([]int, len(src[i]))
        copy(dst[i], src[i])
    }
    return dst
}

func main() {
    original := [][]int{{1, 2}, {3, 4}}
    clone := deepCopy(original)

    // 修改 clone 不會影響 original
    clone[0][0] = 99
    fmt.Println("original:", original) // [[1 2] [3 4]]
    fmt.Println("clone   :", clone)    // [[99 2] [3 4]]
}

關鍵copy 只做 淺層 複製,必須在每一層自行分配新底層陣列才能達到 深拷貝


範例 5:使用 reflect 動態建立任意維度的切片(進階)

package main

import (
    "fmt"
    "reflect"
)

// makeND creates an N‑dimensional slice of ints with the given sizes.
func makeND(sizes ...int) interface{} {
    if len(sizes) == 0 {
        return nil
    }
    // 最內層是 []int
    typ := reflect.TypeOf([]int{})
    // 依序往外包裝 slice
    for i := len(sizes) - 1; i > 0; i-- {
        typ = reflect.SliceOf(typ)
    }
    // 建立最外層的 slice
    v := reflect.MakeSlice(typ, sizes[0], sizes[0])
    // 逐層遞迴建立內層
    var build func(reflect.Value, int)
    build = func(val reflect.Value, dim int) {
        if dim == len(sizes)-1 {
            // 內層已是 []int,直接返回
            return
        }
        for i := 0; i < val.Len(); i++ {
            inner := reflect.MakeSlice(val.Type().Elem(), sizes[dim+1], sizes[dim+1])
            val.Index(i).Set(inner)
            build(inner, dim+1)
        }
    }
    build(v, 0)
    return v.Interface()
}

func main() {
    // 建立 3x2x4 的三維切片
    nd := makeND(3, 2, 4).([][][]int)
    nd[0][1][2] = 7
    fmt.Println(nd[0][1]) // [0 0 7 0]
}

適用情境:當維度在執行時才決定(例如讀取 JSON、CSV)時,可利用 reflect 動態產生任意維度的切片。


常見陷阱與最佳實踐

陷阱 說明 建議的做法
陣列長度寫錯 編譯期錯誤不易發現,尤其在多維時容易寫成 [3][4]int 卻以 [4][3]int 取值。 使用 常數 定義尺寸,並在所有地方保持一致。
切片共享底層陣列 slice2 := slice1[:]; slice2[0] = 99 會同時改變 slice1 若需要獨立資料,使用 深拷貝(如範例 4)。
append 造成重新分配 大量 append 可能導致多次重新分配,效能下降。 先使用 make([]T, 0, 預估容量) 預分配容量,或一次性 append(slice, more...)
不規則多維切片的 nil 切片 var rows [][]int; rows = append(rows, nil) 會產生 nil 切片,遍歷時需檢查。 建立時即分配內層切片,或在使用前 if rows[i] == nil { rows[i] = []int{} }
使用 reflect 失去型別安全 reflect 會讓編譯期檢查失效,容易產生 runtime panic。 僅在 必須 動態決定維度時使用,平常盡量使用具體型別。

最佳實踐

  1. 盡量使用切片:除非資料大小在編譯期已確定且不會變動,否則切片提供更彈性的記憶體管理。
  2. 預先規劃容量make([]T, len, cap) 能減少 append 時的重新配置。
  3. 保持維度一致:在函式介面上明確傳遞 rows, cols,避免硬編碼。
  4. 使用 for range:可同時取得索引與值,減少手動計算錯誤。
  5. 深拷貝時逐層 copy:避免意外共享底層陣列,特別在多執行緒或緩衝區重用時。

實際應用場景

場景 為什麼需要多維結構 可能的實作方式
圖形處理 (Pixel Buffer) 每個像素有 R、G、B 三個通道,常以 height x width x 3 陣列儲存。 使用 [][][3]uint8[][][]uint8 切片,動態調整影像尺寸。
棋盤遊戲 (Go, Chess) 棋盤是固定大小的二維格子,需要快速存取。 [8][8]int 陣列存放棋子代號,或 [][]int 切片支援自訂尺寸。
稀疏矩陣 (Scientific Computing) 大部分元素為 0,僅少數非零值。 使用 不規則切片(jagged array)或 map[int]map[int]float64 以節省記憶體。
資料庫結果集 查詢結果往往是「列」與「欄」的二維表格。 [][]interface{} 或自訂結構切片 []Row 來保存。
機器學習特徵矩陣 每筆樣本有多個特徵,常以 samples x features 矩陣表示。 使用 [][]float64,配合 gonum 套件進行矩陣運算。

總結

  • 多維 陣列 適合「大小固定、效能要求高」的情境,編譯期即能檢查長度。
  • 多維 切片 則提供彈性,支援 不規則動態調整 以及 共享底層 的特性。
  • 正確使用 makeappendcopy,並留意 深拷貝容量預分配,能讓程式在效能與安全性上取得平衡。
  • 在實務開發中,根據資料的 穩定性大小操作頻率,選擇最合適的結構,才能寫出既簡潔又高效的 Go 程式。

祝你在 Golang 的多維資料處理上,能得心應手,寫出更乾淨、更可靠的程式碼!