Go 中如何使用 slice 的长度和容量

GO 刘宇帅 4年前 阅读量: 2086

原文地址:How to use slice capacity and length in Go

简短测试 - 下面代码输出结果是什么?

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],那么你是对的。

什么?为什么不是[0 1 2 3 4] ?

如果你测试没通过,不要担心。这是刚入 Go 的时候非常常见的错误,这篇文章会讲解为什么会有这样的输出结果和如何利用Go语言的一些细微差别让你的代码更加高效。

Slices vs Arrays

Go 即支持 arrays 也支持 slices,刚入门的时候你可能感到混乱,但是相信我你一但开始使用它,那么你会喜欢上它的。

slicesarrays 之间有很多不同,但是这篇文章主要关注的一点是数组的大小是数组类型的一部分,但是 slices 的大小是动态的,因为slice是基于arrays做的封装。

实践中有什么意义呢?比如我们有一个数组val a [10]int,那么这个数组大小就是10,并且不能改变大小。我们调用len(a)那么会一直返回10,因为长度是这个变量类型的一部分。结果就是如果你突然需要一个保存10个以上数量的数组那么你必须重新声明一个新的类型的变量,例如var b [11]int,然后把a里的值全部复制到b

在特定情况下如果可以设置数组的大小是有价值的,通常来说这并不是开发人员需要的。反而他们需要一个可以随着时间增长大小类似于arrays的东西。一种比较粗鲁的实现方式是设置一个远大于你需要的数组大小的数组,然后把这个数组的一个字串作为你需要的数组。下面是一个例子:

var vals [20]int
for i := 0; i < 5; i++ {
    vals[i] = i * i
}
subsetLen := 5

fmt.Println("The subset of out array has a length of :", subsetLen)

// Add a new item to out array
vals[subsetLen] = 123
subsetLen++
fmt.Println("The subset of out array has a length of:", subsetLen)

在这个例子里我们申明的数组大小是20,但是我们只使用了它的一个字串并假定大小是5,当我们往里面新加一个数组之后大小就变成了6。

这就是slices的实现原理(比较粗糙的实现),它包装一个固定大小的数组,就像我们上面设置的一个固定大小为20的array一样。他们记录了字串数据供程序使用,数组里的 length的属性就像我们上面例子里的subsetLen变量一样。

最后,slice 有一个像上面我们申明的 array(20) 的长度一样的容量属性。这是非常有用的,因为这会告诉你在子串增长之前就知道还有多大容量可以使用,slice也是支持的。当容量不够用的时候将会申请一个新的更大容量的数组,当时这些逻辑都是函数append实现了。

总之,sliceappend函数相结合就像数组一样,而且具备随着时间增长的能力。

让我们使用slice代替array做下上面的例子。

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 out 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])

我们仍然可以像使用数组一样从slice里取数据,但是使用sliceappend函数来实现就不需要再考虑底层数组的大小。但是我们仍然可以使用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的slice,slice的容量也会默认设置为5。

然后这看起来和我们开始需要的一样,但是最大的不同就是我们表明了我们需要的slice的大小和容量,在我们初始化slice大小为5之后,我们可以使用append函数往slice添加新的元素,那么slice会增加容量并把新加的元素放在slice后面。

你可以使用Println函数去查看slice的容量的情况。

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]

我们如何打印出正确的结果呢?有几种方法可以做到,我们下面会介绍两种方式,你可以根据你的情况选择其中一个。

直接使用数组索引而不是使用函数 append

第一个解决方案就是仍然使用make函数,但是使用索引去设置值。这样做,我们就得到了下面的代码:

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

在这个例子里我们把值和索引设置的一样,但是你也可以单独设置索引。

例如,如果想要使用map的key作为索引,下面是实例代码:

package main

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 {
        reg[i] = key
        i++
    }
    return ret
}

这个可以正常运行是因为我们知道我们需要的slice的唱的和map的长度一致,所以我们可以使用相应的长度初始化slice并把每个元素设置到相应的索引上。这种方法的缺点就是我们必须随着变量i然后设置相应的值。

这就让我们有了下面的第二种方法

设置长度为0设置容量为你需要的值

宁可记录哪个索引要设置哪个值,不如修改make的调用方式,传递给make两个参数。第一,新的slice的长度设置为0,因为我们还没有往slice添加任何元素。第二,新的slice的容量设置为map的长度,因为我们知道我们需要在slice里添加多少字符串。

下面将创建和上面例子相同slice,现在我们调用append函数,系统知道slice的长度是0,然后把相应的数据放在slice的开头。

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
}

Why do we bother with capacity at all if append handles it?

下面你一定会问”append可以处理slice的容量增长,当时我们为什么还要处理slice的容量呢?"。

事实上,大多数情况下你并不需要关心这个。如果这样你的代码变得比较复杂,那么你就用var vals []ints声明变量并用append处理容量的增长。

但是上面的例子不一样,这种情况下申明容量比较简单。事实上,要确定slice的长度是比较容易的,因为我们知道我们将要把map映射到slice。结果就是我们初始化slice的时候声明了它的容量,这样就能让我们的程序避免申请没有必要的内存。

如果你想要看额外申请的内存长什么样,你可以运行下面的代码看下。每次容量的增长我们的程序都需要申请新的内存。

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 {
        reg = 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
}

在第一个代码中我们申请的slice的大小是0,然后一次是1,2,4,最后是8,意思就是我们需要申请5次内存分配,上面最终需要容量为8的大小的slice存储数据,比我们真正需要容量5要大。

另一方面,我们第二个例子里slice开始和结束的时候容量都是5,并且只需要在keys()函数调用的时候申请一次内存。我们也避免浪费额外的内存并返回一个最合适的大小数组。

不要过分优化

就像我前面说的,我通常不鼓励任何人去优化像这么小的点来提升性能。但是在这种显而易见的情况下,我们强烈建议你去设置一个合适的大小。

这不仅能帮助提升你应用的性能,这能够让代码更加清晰通过显示的声明输入输出的大小关系。

总结

这篇文章不打算详细的介绍slicearray的不同而是要做一个简单的关于slice的长度和容量的介绍以及他们的重要作用。

进一步阅读,我强烈推荐下面来自 Go Blog 的文章:

提示

功能待开通!


暂无评论~