由于@james-henstridge 的回答已经涵盖了如何让它发挥作用,我不会重复他所说的话,但我会解释为什么他的回答有效。
在 Go 中,数组的工作方式与大多数其他语言略有不同(是的,有数组和切片。稍后我将讨论切片)。在 Go 中,数组是固定大小的,正如您在代码中使用的那样(因此,[3]int 与 [4]int 是不同的类型)。此外,数组是值。这意味着如果我将一个数组从一个地方复制到另一个地方,我实际上是在复制该数组的所有元素(而不是像在大多数其他语言中那样,只是对同一个数组进行另一个引用)。例如:
a := [3]int{1, 2, 3} // Array literal
b := a // Copy the contents of a into b
a[0] = 0
fmt.Println(a) // Prints "[0 2 3]"
fmt.Println(b) // Prints "[1 2 3]"
但是,正如您所注意到的,Go 也有切片。切片类似于数组,除了两个关键方面。首先,它们是可变长度的(所以[]int 是任意数量的整数切片的类型)。其次,切片是引用。这意味着当我创建一个切片时,会分配一块内存来表示切片的内容,而切片变量本身实际上只是指向该内存的指针。然后,当我复制那个切片时,我实际上只是在复制指针。这意味着如果我复制切片然后更改其中一个值,我会为每个人更改该值。例如:
a := []int{1, 2, 3} // Slice literal
b := a // a and b now point to the same memory
a[0] = 0
fmt.Println(a) // Prints "[0 2 3]"
fmt.Println(b) // Prints "[0 2 3]"
实施
如果该解释很容易理解,那么您可能还想知道它是如何实现的(如果您无法理解,我会停止阅读这里,因为细节可能会令人困惑)。
在底层,Go 切片实际上是结构。就像我提到的那样,它们有一个指向分配内存的指针,但它们还有另外两个关键组件:长度和容量。如果用 Go 术语来描述,它看起来像这样:
type int-slice struct {
data *int
len int
cap int
}
长度是切片的长度,它的存在是为了让你可以请求len(mySlice),也让 Go 可以检查以确保你没有访问实际上不在切片中的元素。然而,容量有点令人困惑。所以让我们深入一点。
当你第一次创建一个切片时,你给了一些你想要切片的元素。例如,调用 make([]int, 3) 会给你一个 3 个整数的切片。它的作用是在内存中为 3 个 int 分配空间,然后返回一个带有指向数据的指针、长度为 3、容量为 3 的结构。
但是,在 Go 中,您可以执行所谓的切片。这基本上是您从仅代表旧切片的一部分的旧切片创建新切片的地方。您使用slc[a:b] 语法来引用slc 的子切片,从索引a 开始并在索引b 之前结束。所以,例如:
a := [5]int{1, 2, 3, 4, 5}
b := a[1:4]
fmt.Println(b) // Prints "[2 3 4]"
这个切片操作在底层所做的是复制与a 对应的结构,并在内存中编辑指向指向 1 整数的指针(因为新切片从索引 1 开始),并且将长度编辑为比以前短 2(因为旧切片的长度为 5,而新切片的长度为 3)。那么这在现在的记忆中是什么样子的呢?好吧,如果我们可以可视化排列的整数,它看起来像这样:
begin end // a
v v
[ 1 2 3 4 5 ]
^ ^
begin end // b
注意b 结束后还有一个 int 吗?嗯,这就是能力。看,只要内存会一直留给我们使用,我们还不如能够使用它。因此,即使您只有一个长度很小的切片,它也会记住还有更多容量,以防您想要它回来。所以,例如:
a := []int{1, 2, 3}
b := a[0:1]
fmt.Println(b) // Prints "[1]"
b = b[0:3]
fmt.Println(b) // Prints "[1 2 3]"
看看我们最后是怎么做的b[0:3]?在这一点上,b 的长度实际上小于,所以我们能够做到这一点的唯一原因是 Go 一直在跟踪这样一个事实,即在底层内存中,我们'实际上保留了更多容量。这样,当我们要求返还一些时,它可以很高兴地答应。