如何在Go中使用切片容量和長度

來作一個快速測驗-如下代碼輸出什麼?golang

vals := make([]int, 5)
for i := 0; i < 5; i++ {
  vals = append(vals, i)
}
fmt.Println(vals)
複製代碼

Run it on the Go Playground → play.golang.org/p/7PgUqBdZ6…數組

若是猜到了[0 0 0 0 0 0 1 2 3 4],那麼你是正確的。 等一下爲何不是[0 1 2 3 4]app

若是答錯了,也不擔憂。從其餘語言過渡到Go時,這是一個至關廣泛的錯誤,在本文中,咱們將介紹爲何輸出不符合你的預期以及如何利用Go的細微差異來提升代碼效率。函數

Slices vs Arrays

在Go中,既有數組又有切片。切片和數組之間有不少區別,數組的長度是其類型的一部分,因此數組不能改變大小,而切片能夠具備動態大小,由於切片是數組的包裝。這是什麼意思?假設咱們有一個數組var a [10]int。此數組的大小固定,沒法更改。若是咱們調用len(a),它將始終返回10,由於該大小10是該類型[10]int的一部分。若是你在數組中須要10個以上的項,則必須建立一個類型徹底不一樣的新對象,例如var 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)
複製代碼

Run it on the Go Playground → play.golang.org/p/Np6-NEohm…ui

上面代碼中,咱們將一個數組其大小設置爲20,可是因爲咱們僅使用一個子集,所以咱們的代碼能夠僞裝數組的長度爲5,而後在向數組中添加新項後爲6。spa

(很粗略地說)這就是切片的工做方式。它們包裝一個具備設定大小的數組,就像上一個示例中的數組具備20的設定大小同樣。它們還跟蹤程序可以使用的數組子集-length屬性,它相似於上一示例中的subsetLen變量。code

切片還具備一個容量,相似於上一個示例中數組(20)的總長度。這頗有用,由於它告訴你子集能夠增加多大以後才能再也不適合支撐切片的底層數組。當發生這種狀況時,將會分配一個新的數組來支撐切片,可是全部這些邏輯都隱藏在append函數的後面。對象

簡而言之,將sliceappend函數結合在一塊兒能夠爲咱們提供一種與數組很是類似的類型,可是隨着時間的增加,它能夠處理更多元素。

讓咱們再次看一下前面的示例,可是此次咱們將使用切片而不是數組。

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])
複製代碼

Run it on the Go Playground → play.golang.org/p/M_qaNGVbC…

咱們仍然能夠像訪問數組同樣訪問切片中的元素,可是經過使用切片和append函數,咱們再也不須要考慮支持數組的大小。經過使用lencap函數,咱們仍然能夠弄清楚這些事情,可是咱們沒必要太擔憂它們。

考慮到這一點,讓咱們回顧一下文章開頭的測驗代碼,看看出了什麼問題。

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,make 將切片初始化爲[0 ,0 ,0 ,0 ,0]而後繼續調用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)
複製代碼

Run it on the Go Playground → play.golang.org/p/d6OUulTYM…

結果,咱們最終獲得了輸出[0 0 0 0 0 0 0 1 2 3 4]而不是指望的[0 1 2 3 4]。 咱們該如何解決?嗯,有幾種方法能夠作到這一點,咱們將介紹其中兩種,你能夠擇最適合本身狀況的一種。

不使用 append, 直接用索引寫入

第一個解決方法是保持make調用不變,並明確聲明要將每一個元素設置爲的索引。

vals := make([]int, 5)
for i := 0; i < 5; i++ {
  vals[i] = i
}
fmt.Println(vals)
複製代碼

Run it on the Go Playground → play.golang.org/p/JI8Fx3fJC…

咱們設置的值剛好與咱們要使用的索引相同,可是您也能夠獨立跟蹤索引。 例如,若是您想獲取map的key,則可使用如下代碼:

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
}
複製代碼

Run it on the Go Playground → play.golang.org/p/kIKxkdX35…

這之因此行之有效,是由於咱們知道返回的切片的確切長度將與map的長度相同,所以咱們可使用該長度初始化切片,而後將每一個元素分配給適當的索引。這種方法的缺點是咱們必須跟蹤i,以便咱們知道將每一個值放入哪一個索引。

這致使咱們進入第二種方法

使用0做爲長度,並指定容量

咱們更新make調用,在切片類型以後爲其提供兩個參數。首先,新切片的長度將設置爲0,所以咱們沒有在切片中添加任何新元素。第二個參數是新切片的容量,將被設置爲map參數的長度,由於咱們知道切片最終的長度就是 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
}
複製代碼

Run it on the Go Playground → play.golang.org/p/h5hVAHmqJ…

使用 append 能自動擴容,爲何還要關心切片的容量

你可能要問的下一件事是:「若是append函數能夠爲我增長切片的容量,咱們爲何還要告訴程序一個容量?」

事實是,在大多數狀況下,無需太擔憂這一點。若是它使您的代碼複雜得多,只需使用var vals []int初始化切片,而後讓append函數處理繁重的工做。可是針對知道切片最終長度的狀況,咱們能夠在初始化切片時聲明其容量,從而使程序沒必要執行沒必要要的內存分配。

請在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
}
複製代碼

Run it on the Go Playground → play.golang.org/p/fDbAxtAjL…

如今將切片預設容量後將其與上面相同的代碼進行比較:

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
}
複製代碼

Run it on the Go Playground → play.golang.org/p/nwT8X9-7e…

在第一個代碼示例中,咱們的容量從0開始,而後增長到一、二、4,最後是8,這意味着咱們必須在5個不一樣的時間分配一個新數組,此外,最後一個數組用於支持咱們slice的容量爲8,大於咱們最終須要的容量。 另外一方面,咱們的第二個示例以相同的容量(5)開始和結束,而且只須要在keys()函數開始時分配一次便可。咱們還避免浪費任何額外的內存。

不要過分優化

一般不鼓勵任何人擔憂像這樣的次要優化,可是在確實很明顯最終大小應該是多少的狀況下,強烈建議爲切片設置適當的容量或長度。

它不只有助於提升應用程序的性能,並且還能夠經過明確說明輸入大小和輸出大小之間的關係來幫助理清代碼。

本文並非要對切片或數組之間的差別進行詳盡的討論,而只是要簡要介紹容量和長度如何影響切片以及它們在不一樣解決方案中的做用。

相關文章
相關標籤/搜索