向 nil 切片添加一个元素会使容量增加 2

Appending one element to nil slice increases capacity by two

我有一个零切片:

var s1 []int // len(s1) == 0, cap(s1) == 0

我将一个元素附加到:

s2 := append(s1, 1) // len(s2) == 1, cap(s2) == 2

为什么向 nil 切片添加一个元素会使容量增加 2?

使用 fmt.Printf 打印切片显示如下:

[] // s1
[1] // s2

我也很困惑为什么重新切片 s2[0:2] 显示一个零,它不在原始切片中也没有附加到它:

[1,0] // s2[0:2]

容量增长不受用户控制

append(s S, x ...T) S  // T is the element type of S 

If the capacity of s is not large enough to fit the additional values, append allocates a new, sufficiently large underlying array that fits both the existing slice elements and the additional values. Otherwise, append re-uses the underlying array.

参考:https://golang.org/ref/spec#Appending_and_copying_slices
并查看:https://golang.org/doc/effective_go.html#append

不加2(性能优化):
测试初始容量为 5 字节的示例代码,然后它是 16 而不是 10(参见注释输出):

package main

import "fmt"

func main() {
    s := []byte{1, 2, 3, 4, 5}
    fmt.Println(cap(s)) // 5
    s = append(s, s...)
    fmt.Println(cap(s)) // 16
}

测试示例代码(带有注释输出):

package main

import (
    "fmt"
)

func main() {
    s := []int{0}
    fmt.Println(cap(s)) // 1
    s = append(s, s...)
    fmt.Println(cap(s)) // 2
}

测试示例代码(带有注释输出):

package main

import (
    "fmt"
)

func main() {
    s := []int{}
    fmt.Println(cap(s)) // 0
    s = append(s, 1)
    fmt.Println(cap(s)) // 1
}

使用 nil 切片测试示例代码(带有注释输出):

package main

import (
    "fmt"
)

func main() {
    var s []int
    fmt.Println(cap(s)) // 0
    s = append(s, 1)
    fmt.Println(cap(s)) // 1
}

您的示例代码(带有注释输出):

package main

import "fmt"

func main() {
    var s1 []int
    s2 := append(s1, 1)
    fmt.Println(cap(s1)) // 0
    fmt.Println(cap(s2)) // 1
}

使用 5 个整数测试示例代码(带有注释输出):

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    fmt.Println(cap(s)) // 5
    s = append(s, s...)
    fmt.Println(cap(s)) // 10
}

您不能像 s2[1]:
那样访问未初始化的切片索引 恐慌:运行时错误:切片边界超出范围:
测试示例代码(带有注释输出):

package main

import "fmt"

func main() {
    var s1 []int
    s2 := append(s1, 1)

    fmt.Println(cap(s1)) // 0
    fmt.Println(cap(s2)) // 1
    fmt.Println(s1)      // []
    fmt.Println(s2)      // [1]

    //fmt.Println(s2[0:2]) //panic: runtime error: slice bounds out of range
    //fmt.Println(s2[1])   //panic: runtime error: slice bounds out of range

}

Bounds Checking Elimination (or BCE) is a general term for removing redundant bound checking. Normally a go program will panic when a slice or a string is accessed outside of its bounds. There are two types of bound checking: for indexing (a[i]) and for slicing (a[i:j]). The go compiler inserts these bounds checks at every access, but in most cases they are not needed and are redundant based on the context.

Bound checking is important because it provides a defense against buffer overflow attacks and catches a common programming error early. BCE is important because: it speeds up the code, makes the binary smaller. If binaries are slowed down by bound checks then developers will have an incentive to disable bound checking (using -gcflags=-B).

ref

Go 可以免费为您提供超出您要求的容量。这通过减少所需的分配(以及可能的复制)数量来提高性能。容量只是在需要另一次分配之前预留的 space 数量。

如果你给这个slice追加5个元素,至少在我的实验中,容量是8。这应该不足为奇,但也不应该依赖。不同平台,或者不同版本的编译器,实际结果可能不同,只要容量为"sufficiently large"(等于或大于length)即可。

切片的索引上限为defined as its capacity:

For arrays or strings, the indices are in range if 0 <= low <= high <= len(a), otherwise they are out of range. For slices, the upper index bound is the slice capacity cap(a) rather than the length. A constant index must be non-negative and representable by a value of type int; for arrays or constant strings, constant indices must also be in range. If both indices are constant, they must satisfy low <= high. If the indices are out of range at run time, a run-time panic occurs.

这就是为什么阅读超过长度不会引起恐慌的原因。即便如此,您也不应将这些零视为切片的一部分。它们可由切片索引,但 fmt.Printf(s2) 不会正确显示它们,因为它们不是切片的一部分。不要这样下标

一般来说,您要看的是长度,而不是容量。容量主要是可读的,以协助性能优化。

切片零值

切片的零值为零。但这并不意味着你不能用它做任何事情。在 Go 中,类型实际上可以在值为 nil 时使用。例如,即使指针接收者为 nil,也可以调用 struct 方法。这是一个例子:

package main

import "fmt"

type foo struct {
}

func (f *foo) bar() {
    fmt.Println(1)
}

func main() {
    var f *foo
    fmt.Println(f)
    f.bar()
}

playground

切片也是如此。 lencapappend 即使传递 nil 切片也能正常工作。在 append 的情况下,它基本上会为您创建一个新切片,指向一个保存该值的数组。

切片容量增长

当您添加一个元素并需要为其分配更多 space 时,您不会只为一个元素分配 space。这是非常低效的。相反,你分配的比实际需要的多。

究竟分配了多少取决于实现,并且未在语言规范中定义。通常容量会加倍,但在 Go 的情况下,至少从 v1.5 开始,它会四舍五入到分配的内存块。可以找link到源码.

切片过去的长度

实际上支持切片过去的长度。您可以切片超出切片的长度,但不能切片超出容量:

Earlier we sliced s to a length shorter than its capacity. We can grow s to its capacity by slicing it again:

A slice cannot be grown beyond its capacity. Attempting to do so will cause a runtime panic, just as when indexing outside the bounds of a slice or array. Similarly, slices cannot be re-sliced below zero to access earlier elements in the array.

https://blog.golang.org/go-slices-usage-and-internals

在您的情况下,底层数组的容量为 2。您只附加了一个元素,因此另一个元素等于它的零值。当您重新切片超过长度时,Go 可以识别该切片已经具有所需的容量。所以它 returns 一个指向相同数组但长度值设置为 2 的新切片。这是它如何工作的示例:

package main

import "fmt"

func main() {
    var s []int
    s = append(s, 1, 2)
    fmt.Println(s, cap(s))

    s = s[:1]
    fmt.Println(s, cap(s))

    s = s[:2]
    fmt.Println(s, cap(s))
}

playground

它将打印

[1 2] 2
[1] 2
[1 2] 2

你可以看到,即使我重新切片到更小的长度,第二个元素仍然保留。

我认为这里对容量和长度有点混淆。当您打印切片并看到切片中的零个或一个元素时,您正在查看的是它的 length,即切片实际包含的值的数量。底层数组的容量通常对您是隐藏的,除非您使用 cap() 内置函数查找它。

在幕后,切片实际上是固定长度的数组。当你 运行 超出切片中的 space 时,Go 必须通过创建一个新的(更长的)数组并从旧数组复制所有值来使其更大。如果你向一个切片添加很多值,每次为新值分配内存(并复制所有旧值)会非常慢,所以有时 Go 假设你要追加更多元素和继续并分配比它需要的更多的内存,这样你就不必经常复制东西。下次调用 append 时可以使用这个额外的内存,并且在必须扩展之前 可以 存储在切片中的值的数量称为它的 容量。换句话说,切片的容量是切片支持数组的长度,切片的长度与容量无关。

当你将一个值附加到你的切片时,Go 发现它必须为这个值分配 space,所以它分配了它实际需要的 space 数量的两倍,增加了长度1 和容量 2.

你提到的切片是因为切片作用于底层数组:Go 允许你切片超出切片的 length,你不能超出它的 capacity(底层数组的长度)。例如,让我们在一个简单的 nil 切片上尝试一些事情:

var s []int
fmt.Println(len(s), cap(s)) // Prints `0 0` because this is a nil slice
s = append(s, 1)
fmt.Println(len(s), cap(s)) // Prints `1 2`
fmt.Println(s[0:2])         // Prints `[1 0]` (only the first value is part of the slice, the second position in the underlying array is a zero value that is waiting to be used when the slices length grows)
fmt.Println(s[0:3])         // panic: slice bounds out of range (because we've exceeded the slices capacity)