本文 AI 產出,尚未審核

Golang 教學 – 陣列、切片與映射

單元:切片(slices)的基本操作(append、copy、slicing)


簡介

在 Go 語言中,**切片(slice)**是最常使用的集合型別。它在記憶體佈局、動態長度與效能上都比陣列(array)更靈活,同時保有陣列的高效存取特性。無論是處理資料流、實作緩衝區,或是寫演算法,都離不開切片的操作。

本篇文章聚焦於切片的三個核心操作——appendcopy切片(slicing)本身。透過實務範例,我們將說明它們的使用方式、背後的原理,以及在開發過程中常見的陷阱與最佳實踐,幫助讀者從「會用」提升到「會寫、會優化」的層次。


核心概念

1. 切片的結構與底層陣列

切片其實是一個 描述,包含三個欄位:

欄位 說明
指標(ptr) 指向底層陣列的起始位置
長度(len) 目前切片可見的元素數量
容量(cap) 從指標開始,到底層陣列末端的可用空間
// 建立一個底層陣列,並以切片方式引用
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // s -> {2,3,4}, len=3, cap=4
fmt.Printf("len=%d cap=%d slice=%v\n", len(s), cap(s), s)

重點:切片本身不會自行分配記憶體,所有元素仍存放在底層陣列中。只有在需要擴充容量時,Go 會自動 重新配置(re‑allocate) 一塊更大的陣列,並將舊資料複製過去。


2. append – 動態增加元素

append 是最常見的切片操作之一。它會根據切片的 容量 判斷是否需要重新分配底層陣列。

// 範例 1:基本的 append
nums := []int{1, 2, 3}
nums = append(nums, 4, 5) // nums -> {1,2,3,4,5}
fmt.Println(nums)

// 範例 2:append 另一個切片(使用 ... 展開)
more := []int{6, 7, 8}
nums = append(nums, more...) // nums -> {1,2,3,4,5,6,7,8}
fmt.Println(nums)

// 範例 3:觀察容量變化
s := make([]int, 0, 2) // len=0, cap=2
for i := 0; i < 5; i++ {
    s = append(s, i)
    fmt.Printf("i=%d len=%d cap=%d slice=%v\n", i, len(s), cap(s), s)
}

說明

  1. len < capappend 直接在原底層陣列寫入新元素,O(1)
  2. len == cap,Go 會分配 2 倍(或 1.5 倍) 的新容量,將舊資料複製過去,然後再寫入,時間複雜度為 攤銷 O(1)
  3. append 會回傳新切片,一定要把回傳值指派回原變數,否則原切片仍指向舊的底層陣列。

3. copy – 複製切片內容

copy(dst, src) 會把 src 的元素複製到 dst,返回實際複製的元素數量(取兩者長度的較小值)。

// 範例 4:基本的 copy
src := []int{10, 20, 30, 40}
dst := make([]int, 3) // len=3, cap=3
n := copy(dst, src)   // 只會複製前 3 個元素
fmt.Printf("copied=%d dst=%v\n", n, dst)

// 範例 5:重疊區域的 copy(安全的 memmove)
overlap := []int{1, 2, 3, 4, 5}
copy(overlap[1:], overlap) // 變成 {1,1,2,3,4}
fmt.Println(overlap)

說明

  • copy 永遠不會超出目標切片的容量,因此不需要額外的邊界檢查。
  • 當來源與目標切片的底層陣列重疊時,copy 仍會以 安全的方式(類似 memmove)進行,避免資料被覆寫。

4. 切片(slicing)本身的技巧

切片語法 a[low:high] 可以同時指定 起始、結束,也可以省略其中一端:

// 範例 6:不同的切片寫法
data := []int{0, 1, 2, 3, 4, 5}

// 只取前 3 個
first := data[:3] // {0,1,2}

// 只取後 3 個
last := data[3:] // {3,4,5}

// 取中間區段
mid := data[2:5] // {2,3,4}

// 取得完整切片(產生新切片,指向同一底層陣列)
full := data[:]
fmt.Println(first, last, mid, full)

容量的影響

  • slice[low:high]容量cap(slice) - low,即從 low 起始位置到底層陣列末端的長度。
  • 若要限制容量,可使用 三索引切片 a[low:high:max](Go 1.2+):
// 範例 7:三索引切片限制容量
orig := []int{0,1,2,3,4,5,6,7,8,9}
sub := orig[2:6:7] // len=4, cap=5 (從 index 2 到 max-1)
fmt.Printf("len=%d cap=%d sub=%v\n", len(sub), cap(sub), sub)

三索引切片常用於 防止意外的容量擴充(例如在 append 時不想影響原始切片)。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記把 append 回傳值指派回變數 append 可能會產生新底層陣列,未指派會導致資料遺失。 s = append(s, v)
共享底層陣列導致資料被意外修改 多個切片指向同一陣列,對其中一個切片的寫入會影響其他切片。 使用 copy 或三索引切片限制容量。
誤用 copy 的返回值 以為 copy 會回傳錯誤或是全部元素數量。 n := copy(dst, src)n 為實際複製的元素數。
容量過大造成記憶體浪費 連續 append 大量資料時,容量會指數成長。 事先使用 make([]T, 0, 預估大小) 預分配容量。
切片的零值不是 nil 而是 []T{} 雖然兩者在大多數情況下等價,但 nil 切片在 lencap 為 0,且可用於 JSON 編碼等特殊需求。 明確使用 var s []int(nil)或 make([]int, 0)(非 nil)依需求選擇。

最佳實踐

  1. 預先分配容量:對於已知大小的集合,使用 make([]T, 0, n) 可以減少重新分配的次數。
  2. 使用三索引切片 防止意外的 append 擴充。
  3. 盡量避免在迴圈內使用 append 產生大量暫時切片,改用預先分配或 copy
  4. 對外暴露切片時,若不希望呼叫端修改內部資料,應返回 複製的切片append([]T(nil), src...))或使用 只讀介面

實際應用場景

場景 典型操作 範例程式碼
日誌緩衝區 不斷 append 新訊息,達到一定長度後寫入檔案 go\nbuf := make([]string, 0, 100)\nfor _, line := range lines {\n buf = append(buf, line)\n if len(buf) == cap(buf) {\n writeToFile(buf)\n buf = buf[:0] // 重置長度,保留容量\n }\n}\n
資料分頁 使用切片切割大陣列,返回分頁結果 go\nfunc paginate(data []int, page, size int) []int {\n start := page * size\n if start >= len(data) { return nil }\n end := start + size\n if end > len(data) { end = len(data) }\n return data[start:end]\n}\n
併發安全的緩衝區 copy 到本地切片,再傳遞給其他 goroutine,避免競爭條件 go\nfunc broadcast(msg []byte, subs []chan []byte) {\n // 為每個接收者建立獨立副本\n for _, ch := range subs {\n dup := make([]byte, len(msg))\n copy(dup, msg)\n ch <- dup\n }\n}\n
字串切割與重組(使用 []byte appendcopy 結合,實作自訂的 Join go\nfunc join(sep string, parts ...string) string {\n if len(parts) == 0 { return \"\" }\n // 估算總長度\n total := len(parts[0])\n for _, p := range parts[1:] { total += len(sep) + len(p) }\n b := make([]byte, 0, total)\n b = append(b, parts[0]...)\n for _, p := range parts[1:] {\n b = append(b, sep...)\n b = append(b, p...)\n }\n return string(b)\n}\n

總結

切片是 Go 語言中最核心、最具彈性的資料結構。透過 appendcopy 與切片本身的語法,我們可以輕鬆完成動態陣列、資料分頁、緩衝區等多種需求。掌握底層的 長度、容量與底層陣列 概念,才能避免常見的共享與重新配置問題,寫出既正確效能友好的程式碼。

實務建議

  • 預估容量時盡量使用 make,減少不必要的記憶體搬移。
  • 在公開 API 時,返回切片的 副本,保護內部資料不被外部意外修改。
  • 利用三索引切片限制容量,讓 append 的行為更可預測。

只要熟練這些基本操作,您就能在日常開發與大型系統中自如地運用切片,提升程式的可讀性與效能。祝您在 Golang 的旅程中,玩得開心、寫得順手!