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
Tip:
arr[:][:]只是一種語法糖,實際上會先把外層陣列切成切片,再對內層做同樣的切片。
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。 |
僅在 必須 動態決定維度時使用,平常盡量使用具體型別。 |
最佳實踐
- 盡量使用切片:除非資料大小在編譯期已確定且不會變動,否則切片提供更彈性的記憶體管理。
- 預先規劃容量:
make([]T, len, cap)能減少append時的重新配置。 - 保持維度一致:在函式介面上明確傳遞
rows, cols,避免硬編碼。 - 使用
for range:可同時取得索引與值,減少手動計算錯誤。 - 深拷貝時逐層 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 套件進行矩陣運算。 |
總結
- 多維 陣列 適合「大小固定、效能要求高」的情境,編譯期即能檢查長度。
- 多維 切片 則提供彈性,支援 不規則、動態調整 以及 共享底層 的特性。
- 正確使用
make、append、copy,並留意 深拷貝 與 容量預分配,能讓程式在效能與安全性上取得平衡。 - 在實務開發中,根據資料的 穩定性、大小 與 操作頻率,選擇最合適的結構,才能寫出既簡潔又高效的 Go 程式。
祝你在 Golang 的多維資料處理上,能得心應手,寫出更乾淨、更可靠的程式碼!