本文 AI 產出,尚未審核
Golang 教學 – 陣列、切片與映射
單元:切片(slices)的基本操作(append、copy、slicing)
簡介
在 Go 語言中,**切片(slice)**是最常使用的集合型別。它在記憶體佈局、動態長度與效能上都比陣列(array)更靈活,同時保有陣列的高效存取特性。無論是處理資料流、實作緩衝區,或是寫演算法,都離不開切片的操作。
本篇文章聚焦於切片的三個核心操作——append、copy 與 切片(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)
}
說明
- 若
len < cap,append直接在原底層陣列寫入新元素,O(1)。 - 若
len == cap,Go 會分配 2 倍(或 1.5 倍) 的新容量,將舊資料複製過去,然後再寫入,時間複雜度為 攤銷 O(1)。 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 切片在 len、cap 為 0,且可用於 JSON 編碼等特殊需求。 |
明確使用 var s []int(nil)或 make([]int, 0)(非 nil)依需求選擇。 |
最佳實踐
- 預先分配容量:對於已知大小的集合,使用
make([]T, 0, n)可以減少重新分配的次數。 - 使用三索引切片 防止意外的
append擴充。 - 盡量避免在迴圈內使用
append產生大量暫時切片,改用預先分配或copy。 - 對外暴露切片時,若不希望呼叫端修改內部資料,應返回 複製的切片(
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) |
append 與 copy 結合,實作自訂的 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 語言中最核心、最具彈性的資料結構。透過 append、copy 與切片本身的語法,我們可以輕鬆完成動態陣列、資料分頁、緩衝區等多種需求。掌握底層的 長度、容量與底層陣列 概念,才能避免常見的共享與重新配置問題,寫出既正確又效能友好的程式碼。
實務建議:
- 預估容量時盡量使用
make,減少不必要的記憶體搬移。- 在公開 API 時,返回切片的 副本,保護內部資料不被外部意外修改。
- 利用三索引切片限制容量,讓
append的行為更可預測。
只要熟練這些基本操作,您就能在日常開發與大型系統中自如地運用切片,提升程式的可讀性與效能。祝您在 Golang 的旅程中,玩得開心、寫得順手!