定義
Array 是 Golang 的原生型別,其長度固定。
Slice 是 Golang 中長度可以變動的資料型別,也就是可以隨意修改長度的 Array。
其中,Slice 會包含 Pointer、Capacity、Length,Pointer 負責指向實際底層的 Array 位址;Capacity 則是目前 Slice 的容量;Length 則是目前 Slice 內有多少元素在裏頭的長度。
1 | type slice struct { |

宣告方式
1 | []T |
- 普遍使用
make([]type, len, cap),cap通常會在宣告的時候被忽略。 - 直接定義明確資料,
[]string{"Alice", "Bob"}。 - 宣告一個空 Slice,
var member []string。
如果
Capacity沒有被定義,那麼它會與Length一樣,並且會先給定初始值。
可以使用len()來查看 Slice 內的長度,cap()則是查看 Slice 內的容量。
1 | // 法一 make([]type, len, cap) |
基本操作
長度調整
使用 slice[start:end:max] 的方式來去對 Slice 長度進行修改,範圍將會是 [start,end),end 則不會被納入範圍內,並且通常不會用到 max ,除非你想對 Capacity 進行調整。
一旦 Slice 被修改,底層的 Array 元素也會一同被修改。
1 | arr := [4]string{"a","b","c","d"} // len=4, cap=4, value=[a b f d] |
上述的 s2、s3 變數可以看到為什麼明明都是取一樣的長度,可是容量卻不一樣呢?
因為 Slice 是以 start 開始到 原陣列長度 作為其容量大小的預留記憶體,也就是說如果今天有一個 [8]int 要取 array[4:6] 那麼到最後,len 會是 2,則 cap 會是 4。

Append
1 | func append(s []T, x ...T) []T |
對 Slice 進行元素的向後新增,一旦 容量小於 append 後整體的長度,Slice 就會自行擴容,重新宣告一個原本 2 倍 容量大小,將原本的空間 copy 至新的 Slice。
1 | s := make([]int, 0, 1) // [] len=0 cap=1 |
Copy
1 | func copy(dst, src []T) int |
將原本 Slice 的值,深拷貝到另外一個 Slice,return 為已被複製的數量。
如果當
cloneScores容量大於scores複製過來的元素長度,剩下多的空間則是會以 0 來補。
如果當cloneScores容量小於scores複製過來的元素長度,最多就是當前容量而不會擴容,多的元素會被忽略。
1 | func main() { |
Range
用來遍歷 Slice 所使用的關鍵字,提供 key 和 value,如果某一個值不需要使用到,可以使用 _ 來代替。
1 | func main() { |
growslice 擴容
為了更好地去了解 Slice 的擴容原理,我寫了一個 for 迴圈在 Go Playground ,來看看到底是不是原始碼所寫的那樣,測試程式碼邏輯是每次只要 Slice 長度等於容量時,就會去打印當前的長度與容量。
1 | func main() { |
結果非常出乎意料,竟然在容量 512 要進行擴容時,擴充成 848,於是我去翻找了原始碼中的 runtime.growslice 函數做了什麼:
1 | // oldPtr = array 的指標位址 |
發現在進行擴容時會有三大步驟:
- 初步確認擴容大小
- 記憶體對齊
- 重新分配新的記憶體,並將新舊 Slice 資料複製
初步確認擴容大小
1 | func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice { |
這邊我舉出兩個資料型別大小為 0 範例,空 Slice 和 struct 型別的 Slice:
1 | type empty struct{} |
根據 1.22 版本的 runtime.nextslicecap:
1 | // newLen = 新的 Slice 長度 (= oldLen + num) |
而在 1.18 版本以前計算方式則不太一樣:
1 | newcap := old.cap |
1.18 版本以前 的邏輯:
新增元素後的長度 > 2 倍原始容量,那麼就會使用該長度。- 擴容在
少量元素的情況下,會以2 倍大小作為新的容量,但是為了避免後續容量增加而未使用造成的空間浪費,所以只要是容量大小 >= 1024 的情況,就只會每次增加原本容量的 25%。
1.18 版本以後 的邏輯,則是 修改閥值大小 把 1024 降低到 256,並且每次新增除了 25% 的容量,還會新增 192 的空間,會這樣更動的原因可以查看當時的 Commit 紀錄,其實就是想要讓 擴容曲線更加圓滑。
| starting cap | growth factor |
|---|---|
| 256 | 2.0 |
| 512 | 1.63 |
| 1024 | 1.44 |
| 2048 | 1.35 |
| 4096 | 1.30 |
那麼根據 1.22 版本的規則 for 迴圈的範例,從容量 512 擴容時,newcap 應該要是 512 * 1.25 + 192 = 832,為什麼打印出來的容量是 848 呢?那是因為還要對 記憶體空間 進行 對齊。
記憶體對齊
關於記憶體對齊,jserv 在 你所不知道的 C 語言 的講座有講到,大概意思就是每次 CPU 都會抓固定的大小 (32 位元就是 4 bytes、64 位元就是 8 bytes) 進行運算。
假定我現在是 64 位元的系統,我每次以 8 bytes 為單位的方式抓資料,如果我現在要抓的資料位址在第 1~8 byte,那麼就要抓兩次分別是:
- 取 0-7 byte,把第 0 byte 的資料清除,留下 1-7 byte
- 取 8-15 byte,把 9-15 byte 的資料清除,留下第 8 byte
- 再把抓出來的兩個記憶體位址進行拼接
同樣是取資料,但是由於
記憶體的資料不對齊,所以操作更加複雜導致效率低落
所以我們在擴容的時候,當然也要根據 Slice 的型別佔用的空間 去申請 相對應大小的空間。
1 | func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice { |
因為範例中是使用 int 型別,在 64 位元下佔用 8 bytes,且 goarch.PtrSize 也是 8 bytes。如果將數字帶入的話:
1 | lenmem = uintptr(512) * 8 // = 4096 |
後面計算到一半使用 roundupsize(),那麼我們再去看runtime.roundupsize做了什麼運算:
1 | // _MaxSmallSize = 32 << 10 = 32768 |
我們因為會使用到 6656 bytes 的空間,所以要被 divRoundUp() 運算,runtime.divRoundUp:
1 | // divRoundUp returns ceil(n / a). |
運算結果為 (5632 + 128 - 1) / 128 = 44,就會變成 class_to_size[size_to_class128[44]],找出runtime.size_to_class8 和 runtime.size_to_class128:
size_to_class128:代表要多少 128 bytes 來對齊記憶體class_to_size:代表對應記憶體對其的字節大小
1 | var class_to_size = [0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768] |
所以 size_to_class128[44] = 49,接續 class_to_size[49] = 6784。最後得知要申請的新記憶體空間大小就會是 6784 bytes,而容量就會是 6784 / 8 = 848。
1 | newcap = int(capmem / 8) // = int(6784/8) = 848 |
重新分配新的記憶體,並將新舊 Slice 資料複製
最後一步,也就是根據需要的空間大小去 申請記憶體空間,根據把舊資料 移植 到新的記憶體位址,返回新的 Slice。
1 | func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice { |
小試身手
請都以 1.18 以後的版本邏輯,回答以下會輸出什麼!
問題一
1 | func main() { |
答案一
1 | lenth: 1 |
問題二
1 | func main() { |
答案二
1 | len(pollorder) = 5 |
問題三
1 | func main() { |
答案三
1 | cap of s1:6 |
結論
- Slice 使用
- 建立 Slice 可以根據實際需求預先劃分好
需要的容量以及初始值。 - Slice 在進行
copy時,要判斷是否目的 Slice 是否為需求所要。 - Slice 指向同一個 Array,請避免多次操作同一個 Slice。
- 使用
len()、cap()可以獲取 Slice 的長度和容量。 - Slice 在賦值時不會複製整個 Slice,而是獲取其
Array 位址、長度和容量。 - 使用
append可能會觸發擴容,若是想要避免,可以提前宣告足夠的容量。
- 建立 Slice 可以根據實際需求預先劃分好
- growslice
- 1.18 前後版本的擴容閥值不同,原本由
1024降至256。 - 如果
新增元素後長度 > 原本容量 2 倍,就會使用該長度。 - 如果要
原本容量 < 閥值,就會以2 倍進行擴容。 - 如果
原本容量 >= 閥值,則是根據原本容量的 25%進行擴容,新版本還會增加 192。 - 計算好預期容量後,還要讓
記憶體對齊,才能夠申請新的 Slice。
- 1.18 前後版本的擴容閥值不同,原本由
本篇文章的程式碼範例。
參考資料
- https://juejin.cn/post/6997212552241348616
- https://pjchender.dev/golang/slice-and-array/
- https://www.joxrays.com/go-slice/
- https://golang.design/go-questions/slice/grow/
- https://github.com/golang/go