我有一个零片:
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
A <input data-bind="value: a, valueUpdate: 'afterkeydown'"> = (B x C) <span data-bind="text: b()*c()"></span><br />
B <input data-bind="value: b, valueUpdate: 'afterkeydown'"> = (A / C) <span data-bind="text: a()/c()"></span><br />
C <input data-bind="value: c, valueUpdate: 'afterkeydown'"> = (A / B) <span data-bind="text: a()/b()"></span><br />
我将一个元素添加到:
var s1 []int // len(s1) == 0, cap(s1) == 0
为什么在nil切片上附加一个元素会使容量增加2?
使用s2 := append(s1, 1) // len(s2) == 1, cap(s2) == 2
打印切片显示以下内容:
fmt.Printf
我也很困惑为什么重新切片[] // s1
[1] // s2
会显示一个零,这个零不在原始切片中,也不会附加到原始切片中:
s2[0:2]
答案 0 :(得分:15)
Go可以免费为您提供超出您要求的容量。这通过减少所需的分配(以及可能的复制)数量来提高性能。容量只是在需要进行另一次分配之前预留的空间量。
如果你向这个切片添加5个元素,至少在我的实验中,容量是8.这不应该是令人惊讶的,但也不应该依赖。在不同平台或编译器的不同版本上,只要容量“足够大”(等于或大于长度),实际结果可能会有所不同。
切片的上部索引范围是defined as its capacity:
对于数组或字符串,如果0 <=低&lt; =高&lt; = len(a),则索引在范围内,否则它们超出范围。对于切片,上限索引是切片容量上限(a)而不是长度。常量索引必须是非负的,并且可以通过int类型的值表示;对于数组或常量字符串,常量索引也必须在范围内。如果两个指数都是常数,则它们必须满足低<=高。如果索引在运行时超出范围,则会发生运行时混乱。
这就是为什么阅读超过长度不会导致恐慌。即便如此,你也不应该把那些零视为切片的一部分。它们可以通过切片进行索引,但fmt.Printf(s2)
将无法正确显示它们,因为它们不是切片的一部分。不要这样下标。
一般来说,你想看长度,而不是容量。容量主要是可读的,有助于性能优化。
答案 1 :(得分:4)
我认为,这里有一些关于容量和长度的混淆。当您打印切片并在切片中看到零个或一个元素时,您正在查看的是长度,即切片实际包含的值的数量。除非您使用内置的cap()
查找,否则基础数组的容量通常是隐藏的。
在引擎盖下,切片实际上是固定长度的数组。当切片中的空间不足时,Go必须通过创建一个新的(更长的)数组并将所有值从旧数组中复制来使其变大。如果您要为切片添加大量值,那么每次为新值分配内存(并复制所有旧值)将会非常缓慢,因此有时候Go会假设您要去添加更多元素并继续进行并分配比所需更多的内存,这样您就不必经常复制内容。这个额外的内存可以在下次调用append时使用,并且可以在必须扩展之前存储在切片中的值的数量称为 capacity 。换句话说,切片的容量是切片支持阵列的长度,切片的长度与容量无关。
当您向切片附加单个值时,Go会看到它必须为此值分配空间,因此它会分配两倍于实际需要的空间量,将长度增加1,将容量增加2。
你提到的切片是因为切片作用于底层数组:Go允许你切片超出切片的长度,你只是不能超越其容量(底层数组的长度)。例如,让我们在一个简单的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)
答案 2 :(得分:1)
切片的零值为零。但这并不意味着你无法用它做任何事情。在Go中,当值为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()
}
同样适用于切片。 len
,cap
,append
即使您通过了零片,也都可以正常工作。在append
的情况下,它基本上为您创建一个新切片,指向一个包含该值的数组。
添加元素并且需要为其分配更多空间时,不会仅为一个元素分配空间。这非常缺乏。相反,你分配的不仅仅是实际需要的。
究竟分配了多少取决于实现,并且未在语言规范中定义。通常容量加倍但是在Go的情况下,至少从v1.5开始,它向上舍入到分配的内存块。您可以找到源代码here的链接。
实际上支持切片过去的长度。您可以切片超出切片的长度,但不能切片超出容量:
早些时候我们将s切成比其容量短的长度。我们可以成长 通过再次切片来达到它的能力:
切片不能超出其容量。试图这样做会 导致运行时混乱,就像在a的边界之外索引时一样 切片或数组。同样,切片不能在零以下重新切片 访问数组中的早期元素。
https://blog.golang.org/go-slices-usage-and-internals
在你的情况下,底层数组的容量为2.你只附加一个元素,所以另一个元素等于它的零值。当你重新过去长度时,Go可以识别出切片已经具有所需的容量。所以它返回一个指向同一个数组的新切片,但长度值设置为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))
}
会打印
[1 2] 2
[1] 2
[1 2] 2
你可以看到,即使我重新缩小到较小的长度,第二个元素仍然保留。
答案 3 :(得分:0)
容量增长不受用户控制:
append(s S, x ...T) S // T is the element type of S
如果s的容量不足以容纳附加值, append分配一个适合的新的,足够大的底层数组 现有的切片元素和附加值。除此以外, append重新使用底层数组。
参考: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]
之类切片的未初始化索引:
panic:运行时错误:切片范围超出范围:
测试示例代码(带注释输出):
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(或BCE)是删除的一般术语 冗余绑定检查。通常一个go程序会在a时出现恐慌 切片或字符串在其边界之外访问。那里有两个 绑定检查的类型:用于索引(a [i])和切片(a [i:j])。 go编译器在每次访问时插入这些边界检查,但是在 大多数情况下,它们不是必需的,并且根据上下文是多余的。
绑定检查很重要,因为它提供了防御 缓冲区溢出攻击并尽早发现常见的编程错误。 BCE很重要,因为:它加速了代码,制作了二进制文件 小。如果通过绑定检查减慢二进制文件然后开发人员 将有动机禁用绑定检查(使用-gcflags = -B)。