如何在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()函数开始时分配一次便可。咱们还避免浪费任何额外的内存。

不要过分优化

一般不鼓励任何人担忧像这样的次要优化,可是在确实很明显最终大小应该是多少的状况下,强烈建议为切片设置适当的容量或长度。

它不只有助于提升应用程序的性能,并且还能够经过明确说明输入大小和输出大小之间的关系来帮助理清代码。

本文并非要对切片或数组之间的差别进行详尽的讨论,而只是要简要介绍容量和长度如何影响切片以及它们在不一样解决方案中的做用。

相关文章
相关标签/搜索