#快速測試 - 下面的程式碼輸出什麼?
vals := make([]int, 5) for i := 0; i < 5; i { vals = append(vals, i) } fmt.Println(vals)
如果你猜的是 [0 0 0 0 0 0 1 2 3 4],那你是對的。
如果你在測驗中做錯了,你也不用擔心。這是在過渡到 Go 語言的過程中相當常見的錯誤,在這篇文章中,我們將說明為什麼輸出不是你預期的,以及如何利用 Go 的細微差別來使你的程式碼更有效率。
在 Go 中同時有陣列(array)和切片(slice)。這可能令人困惑,但一旦你習慣了,你會喜歡上它。請相信我。
切片和數組之間存在許多差異,但我們要在本文中重點介紹的內容是數組的大小是其類型的一部分,而切片可以具有動態大小,因為它們是圍繞數組的封裝。
這在實踐中意味著什麼?那麼假設我們有數組 val a [10]int。此數組具有固定大小,且無法變更。如果我們呼叫 len(a),它總是會回傳 10,因為這個大小是類型的一部分。因此,如果你突然需要在數組中超過 10 個項,則必須建立一個完全不同類型的新對象,例如 val b [11]int,然後將所有值從 a 複製到 b。
在特定情況下,含有集合大小的陣列是有價值的,但一般而言,這不是開發人員想要的。相反,他們希望在 Go 中使用類似於數組的東西,但是隨著時間的推移,它們能夠隨時增長。一個粗略的方式是建立一個比它需要大得多的數組,然後將數組的子集視為數組。下面的程式碼就是個例子。
var vals [20]int for i := 0; i < 5; i { vals[i] = i * i } subsetLen := 5 fmt.Println("The subset of our array has a length of:", subsetLen) // Add a new item to our array vals[subsetLen] = 123 subsetLen fmt.Println("The subset of our array has a length of:", subsetLen)
在程式碼中,我們有一個長度為20 的數組,但由於我們只使用一個子集,程式碼中我們可以假定數組的長度是5,然後在我們向數組中添加一個新的項之後是6 。
這是(非常粗略地說)切片是如何工作的。它們包含一個具有設定大小的數組,就像我們前面的例子中的數組一樣,它的大小為 20。
它們也追蹤程式中使用的陣列的子集 - 這就是 append 屬性,它類似於上一個例子中的 subsetLen 變數。
最後,一個切片還有一個 capacity,類似於前面範例中我們的陣列的總長度(20)。這是很有用的,因為它會告訴你的子集在無法容納切片數組之前可以增長的大小。當發生這種情況時,需要指派一個新的數組,但所有這些邏輯都隱藏在 append 函數的後面。
簡而言之,使用 append 函數組合切片給我們一個非常類似於陣列的類型,但隨著時間的推移,它可以處理更多的元素。
我們再來看前面的例子,但這次我們將使用切片而不是陣列。
var vals []int for i := 0; i < 5; i { vals = append(vals, i) fmt.Println("The length of our slice is:", len(vals)) fmt.Println("The capacity of our slice is:", cap(vals)) } // Add a new item to our array vals = append(vals, 123) fmt.Println("The length of our slice is:", len(vals)) fmt.Println("The capacity of our slice is:", cap(vals)) // Accessing items is the same as an array fmt.Println(vals[5]) fmt.Println(vals[2])
我們仍然可以像數組一樣存取我們的切片中的元素,但是透過使用切片和 append 函數,我們不再需要考慮背後數組的大小。我們仍然可以透過使用 len 和 cap 函數來計算這些東西,但是我們不用擔心太多。簡潔吧?
記住這一點,讓我們回顧前面的測試,看下什麼出錯了。
vals := make([]int, 5) for i := 0; i < 5; i { vals = append(vals, i) } fmt.Println(vals)
當呼叫 make 時,我們允許最多傳入 3 個參數。第一個是我們指派的類型,第二個是類型的“長度”,第三個是類型的“容量”(這個參數是可選的)。
透過傳遞參數 make([]int, 5),我們告訴程式我們要建立一個長度為 5 的切片,在這種情況下,預設的容量與長度相同 - 本例中是 5。
雖然這可能看起來像我們想要的那樣,這裡的重要區別是我們告訴我們的切片,我們要將「長度」和「容量」設為5,假設你想要在初始的5 個元素之後加入新的元素,我們接著呼叫append 函數,那麼它會增加容量的大小,並且會在切片的最後添加新的元素。
如果在程式碼中加入一條 Println() 語句,你可以看到容量的變化。
vals := make([]int, 5) fmt.Println("Capacity was:", cap(vals)) for i := 0; i < 5; i { vals = append(vals, i) fmt.Println("Capacity is now:", cap(vals)) } fmt.Println(vals)
最後,我們最終得到 [0 0 0 0 0 0 1 2 3 4] 的輸出而不是希望的 [0 1 2 3 4]。
要如何修復它呢?好的,這有幾種方法,我們將講解兩種,你可以選取任何一種在你的場景中最有用的方法。
#第一種修復是保留 make 呼叫不變,並且明確地使用索引來設定每個元素。這樣,我們就得到如下的程式碼:
vals := make([]int, 5) for i := 0; i < 5; i { vals[i] = i } fmt.Println(vals)
在這種情況下,我們設定的值恰好與我們要使用的索引相同,但是你也可以獨立追蹤索引。
例如,如果你想要取得 map 的鍵,你可以使用下面的程式碼。
package main import "fmt" func main() { fmt.Println(keys(map[string]struct{}{ "dog": struct{}{}, "cat": struct{}{}, })) } func keys(m map[string]struct{}) []string { ret := make([]string, len(m)) i := 0 for key := range m { ret[i] = key i } return ret }
這樣做很好,因為我們知道我們傳回的切片的長度將與 map 的長度相同,因此我們可以用該長度初始化我們的切片,然後將每個元素分配到適當的索引中。這種方法的缺點是我們必須追蹤 i,以便了解每個索引要設定的值。
這就讓我們引出了第二種方法…
與其追蹤我們要新增的值的索引,我們可以更新我們的 make 調用,並在切片類型之後提供兩個參數。第一個,我們的新切片的長度將被設定為 0,因為我們還沒有添加任何新的元素到切片中。第二個,我們新切片的容量將被設定為 map 參數的長度,因為我們知道我們的切片最終會添加許多字串。
這會如前面的例子那樣仍舊會在背後建立相同的數組,但是現在當我們呼叫 append 時,它會將它們放在切片開始處,因為切片的長度是 0。
package main import "fmt" func main() { fmt.Println(keys(map[string]struct{}{ "dog": struct{}{}, "cat": struct{}{}, })) } func keys(m map[string]struct{}) []string { ret := make([]string, 0, len(m)) for key := range m { ret = append(ret, key) } return ret }
接下來你可能會問:「如果 append 函數可以為我增加切片的容量,那我們為什麼要告訴程式容量呢?」
事實是,在大多數情況下,你不必擔心這太多。如果它讓你的程式碼變得更複雜,只需用 var vals []int 初始化你的切片,然後讓 append 函數處理接下來的事。
但這種情況是不同的。它並不是聲明容量困難的例子,實際上這很容易確定我們的切片的最後容量,因為我們知道它將直接映射到提供的 map 中。因此,當我們初始化它時,我們可以聲明切片的容量,並免於讓我們的程式執行不必要的記憶體分配。
如果要查看額外的記憶體分配情況,請在 Go Playground 上執行以下程式碼。每次增加容量,程式都需要做一次記憶體分配。
package main import "fmt" func main() { fmt.Println(keys(map[string]struct{}{ "dog": struct{}{}, "cat": struct{}{}, "mouse": struct{}{}, "wolf": struct{}{}, "alligator": struct{}{}, })) } func keys(m map[string]struct{}) []string { var ret []string fmt.Println(cap(ret)) for key := range m { ret = append(ret, key) fmt.Println(cap(ret)) } return ret }
現在將此與相同的程式碼進行比較,但具有預先定義的容量。
package main import "fmt" func main() { fmt.Println(keys(map[string]struct{}{ "dog": struct{}{}, "cat": struct{}{}, "mouse": struct{}{}, "wolf": struct{}{}, "alligator": struct{}{}, })) } func keys(m map[string]struct{}) []string { ret := make([]string, 0, len(m)) fmt.Println(cap(ret)) for key := range m { ret = append(ret, key) fmt.Println(cap(ret)) } return ret }
在第一個程式碼範例中,我們的容量從0 開始,然後增加到1、 2、 4, 最後是8,這意味著我們必須分配5 次數組,最後一個容納我們切片的陣列的容量是8,這比我們最終需要的要大。
另一方面,我們的第二個例子開始和結束都是相同的容量(5),它只需要在 keys() 函數的開頭分配一次。我們還避免了浪費任何額外的內存,並返回一個能放下這個數組的完美大小的切片。
#如前所述,我通常不鼓勵任何人做這樣的小優化,但如果最後大小的效果真的很明顯,那麼我強烈建議你嘗試為切片設定適當的容量或長度。
這不僅有助於提高程式的效能,還可以透過明確說明輸入的大小和輸出的大小之間的關係來幫助澄清你的程式碼。
本文並不是對切片或陣列之間差異的詳細討論,而是簡要介紹了容量和長度如何影響切片,以及它們在方案中的用途。
以上是在 Go 中使用切片的容量和長度的技巧的詳細內容。更多資訊請關注PHP中文網其他相關文章!