Go 中切片(Slice)的長度與容量
切片長度與容量在 Go 中很常見。切片長度是切片中可用元素的數量,而切片容量是從切片中第一個元素開始計算的底層數組中的元素數量。
Go 中的開發者經常混淆切片長度和容量,或者對它們不夠了解。理解這兩個概念對于高效處理切片的核心操作,比如切片的初始化、使用 append 添加元素、復制或切片操作等,至關重要。對這些概念的誤解可能導致切片的不合理使用,甚至造成內存泄漏。
在 Go 中,切片是由數組支持的。這意味著切片的數據以連續的方式存儲在數組數據結構中。切片還負責在底層數組已滿時添加元素,或在幾乎為空時縮減底層數組。
在內部,切片包含指向底層數組的指針,以及長度和容量。長度表示切片包含的元素數量,而容量表示底層數組中的元素數量,從切片中的第一個元素開始計算。讓我們通過一些示例來更清楚地了解這些概念。首先,讓我們使用給定的長度和容量初始化一個切片:
s := make([]int, 3, 6) // Three-length, six-capacity slice
第一個參數,表示長度,是必須的。但是,第二個參數表示容量是可選的。圖1展示了此代碼在內存中的結果。
Figure 1 — 一個長度為3、容量為6的切片
在這種情況下,make 創建了一個包含六個元素的數組(容量)。但由于長度設置為3,Go 只初始化了前三個元素。另外,因為切片是 []int 類型,所以前三個元素被初始化為 int 類型的零值:0。灰色元素已經分配但尚未使用。
如果我們打印這個切片,會得到長度范圍內的元素 [0 0 0]。如果我們將 s[1] 設為1,切片的第二個元素會更新,但不會影響其長度或容量。圖2說明了這一點。
圖2 — 更新切片的第二個元素:s[1] = 1
然而,訪問超出長度范圍之外的元素是被禁止的,即使它在內存中已經分配。例如,s[4] = 0 會導致以下 panic:
panic: runtime error: index out of range [4] with length 3
我們如何使用切片剩余的空間呢?通過使用內置函數 append:
s = append(s, 2)
這段代碼向現有的 s 切片追加了一個新元素。它使用了第一個灰色元素(已分配但尚未使用)來存儲元素2,正如圖3所示。
圖3 — 向 s 切片追加一個元素
切片的長度從3更新為4,因為現在切片包含了四個元素。現在,如果我們再添加三個元素以至于后臺數組不夠大,會發生什么?
s = append(s, 3, 4, 5)
fmt.Println(s)
如果我們運行這段代碼,會看到切片能夠滿足我們的請求:
[0 1 0 2 3 4 5]
因為數組是一個固定大小的結構,在第4個元素之前,它能夠存儲新的元素。當我們想要插入第5個元素時,數組已經滿了:Go 內部會創建另一個數組,將所有元素復制過去,然后再插入第5個元素。圖4展示了這個過程。
圖4 — 因為初始的后臺數組已滿,Go 創建了另一個數組并復制了所有元素。
現在切片引用了新的后臺數組。之前的后臺數組會怎樣呢?如果它不再被引用,它最終會被垃圾收集器(GC)釋放,如果它是在堆上分配的話(我們在錯誤#95 “不理解堆棧與堆的區別”中討論了堆內存,并在錯誤#99 “不理解GC的工作原理”中討論了GC的工作原理)。
對切片進行切片操作會發生什么?切片是對數組或切片進行的操作,提供了一個左閉右開的范圍;第一個索引是包括的,而第二個索引是排除的。以下示例展示了影響,并在圖5中顯示了內存中的結果:
s1 := make([]int, 3, 6) // Three-length, six-capacity slice
s2 := s1[1:3] // Slicing from indices 1 to 3
圖5 — 切片 s1 和 s2 引用相同的后臺數組,但長度和容量不同
首先,s1 是一個長度為3、容量為6的切片。當通過對 s1 進行切片創建 s2 時,兩個切片都引用同一個后臺數組。但是,s2 從不同的索引開始,即索引1。因此,它的長度和容量(長度為2,容量為5)與 s1 不同。如果我們更新 s1[1] 或 s2[0],則更改會作用于同一個數組,因此在兩個切片中都是可見的,如圖6所示。
圖6 — 因為 s1 和 s2 共享同一個數組,更新共同的元素會使兩個切片中的更改都可見
現在,如果我們向 s2 添加一個元素會發生什么?以下代碼會同時改變 s1 嗎?
s2 = append(s2, 2)
共享的后臺數組被修改,但只有 s2 的長度發生了變化。圖7展示了向 s2 添加元素的結果。
圖7 — 向 s2 添加元素
s1 仍然是一個長度為3、容量為6的切片。因此,如果我們打印 s1 和 s2,添加的元素只會在 s2 中可見:
s1=[0 1 0], s2=[1 0 2]
很重要理解這種行為,這樣我們在使用 append 時就不會形成錯誤的假設。
注意: 在這些示例中,后臺數組是內部的,不直接對 Go 開發者可見。唯一的例外是從現有數組切片創建切片。
還有最后一件事需要注意:如果我們不斷向 s2 中添加元素,直到后臺數組滿為止,內存狀態會是怎樣的?讓我們再添加三個元素,以便后臺數組沒有足夠的容量:
s2 = append(s2, 3)
s2 = append(s2, 4) // At this stage, the backing is already full
s2 = append(s2, 5)
這段代碼導致創建了另一個后臺數組。圖 8 展示了內存中的結果。
圖 8 — 向 s2 添加元素直到后臺數組已滿
s1 和 s2 現在引用兩個不同的數組。由于 s1 仍然是一個三長度、六容量的切片,它仍然有一些可用緩沖區,因此它繼續引用最初的數組。而且,新的后臺數組是通過從 s2 的第一個索引復制初始數組而生成的。這就是為什么新數組從元素 1 開始,而不是 0。
結論
總結一下,切片長度 是切片中可用元素的數量,而 切片容量 是后臺數組中的元素數量。向一個已滿的切片(長度 == 容量)添加元素會導致創建一個新的后臺數組,將之前數組中的所有元素復制到新數組中,并更新切片指向新數組。