切片
在Go语言中,我们很少直接使用数组。取而代之的是使用切片。切片是轻量的包含并表示数组的一部分的结构。 这里有几种创建切片的方式,我们来看看什么情况下使用它们。首先在数组的基础之上进行一点点变化:
scores := []int{1,4,293,4,9}
和数组声明不同的是,我们的切片没有在方括号中定义长度。为了理解两者的不同,我们来看看另一种使用make
来创建切片的方式:
scores := make([]int, 10)
我们使用 make
关键字代替 new
, 是因为创建一个切片不仅是只分配一段内存(这个是 new
关键字的功能)。具体来讲,我们必须要为一个底层数组分配一段内存,同时也要初始化这个切片。在上面的代码中,我们初始化了一个长度是 10 ,容量是 10 的切片。长度是切片的长度,容量是底层数组的长度。在使用 make
创建切片时,我们可以分别的指定切片的长度和容量:
scores := make([]int, 0, 10)
上面的代码创建了一个长度是 0 ,容量是 10 的切片。(如果你仔细观察的话,你会注意到 make
和 len
被重载了。Go 的一些特性没有暴露出来给开发者使用,这也许会让你感到沮丧。)
为了更好的理解切片的长度和容量之间的关系,我们来看下面的的例子:
func main() {
scores := make([]int, 0, 10)
scores[7] = 9033
fmt.Println(scores)
}
我们上面的这个例子不能运行,为什么呢?因为切片的长度是 0 。没错,底层数组可以放 10 个元素,但是我们需要显式的扩展切片,才能访问到底层数组的元素。一种扩展切片的方式是通过 append
的关键字来实现:
func main() {
scores := make([]int, 0, 10)
scores = append(scores, 5)
fmt.Println(scores) // prints [5]
}
但是那并没有改变原始代码的意图。追加一个值到长度为0的切片中将会设置第一个元素。无论什么原因,我们崩溃的代码想去设置索引为7的元素值。为了实现这个,我们可以重新切片:
func main() {
scores := make([]int, 0, 10)
scores = scores[0:8]
scores[7] = 9033
fmt.Println(scores)
}
我们可以调整的切片大小最大范围是多少呢?达到它的容量,这个例子中,是10。你可能在想 这实际上并没有解决数组固定长度的问题。但是 append
是相当特别的。如果底层数组满了,它将创建一个更大的数组并且复制所有原切片中的值(这个就很像动态语言 PHP,Python,Ruby,JavaScript 的工作方式)。这就是为什么上面的例子中我们必须重新将 append
返回的值赋值给 scores
变量:append
可能在原有底层数组空间不足的情况下创建了新值。
如果我告诉你 Go 使用 2x 算法来增加数组长度,你猜下面将会打印什么?
func main() {
scores := make([]int, 0, 5)
c := cap(scores)
fmt.Println(c)
for i := 0; i < 25; i++ {
scores = append(scores, i)
// 如果容量改变了
// Go 必须增加数组长度来容纳新的数据
if cap(scores) != c {
c = cap(scores)
fmt.Println(c)
}
}
}
初始 scores
的容量是5。为了存储25个值,它必须扩展三次容量,分别是 10,20,最终是40。
最后一个例子,考虑这个:
func main() {
scores := make([]int, 5)
scores = append(scores, 9332)
fmt.Println(scores)
}
这里输出是 [0, 0, 0, 0, 0, 9332]
,可能你觉得是[9332, 0, 0, 0, 0]
?对一个用户而言,这可能逻辑上是正确的。然而,对于一个编译器,你告诉他的是追加一个值到一个已经有5个值的切片。
最终,这有四种方式初始化一个切片:
names := []string{"leto", "jessica", "paul"}
checks := make([]bool, 10)
var names []string
scores := make([]int, 0, 20)
什么时候该用哪个呢?第一个不用过多解释。当你事先知道数组中的值的时候,你可以使用这个方式。
当你想要写入切片具体的索引时,第二个方法很有用,例如:
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, len(saiyans))
for index, saiyan := range saiyans {
powers[index] = saiyan.Power
}
return powers
}
第三个版本是指向空的切片,用于当元素数量未知时与 append
连接。
最后一个版本是让我们声明一个初始的容量。如果我们大概知道元素的数量将是很有用的。
即使当你知道大小的时候,append
也可以使用,取决于个人偏好:
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, 0, len(saiyans))
for _, saiyan := range saiyans {
powers = append(powers, saiyan.Power)
}
return powers
}
切片作为数组的包装是一个很强大的概念。许多语言有切片数组的概念。JavaScript 和 Ruby 数组都有一个 slice
方法。Ruby 中你可以使用 [START..END]
获取一个切片,或者 Python 中可以通过 [START:END]
实现。然而,在这些语言中,一个切片实际上是复制了原始值的新数组。如果我们使用 Ruby,下面这段代码的输出是什么呢?
scores = [1,2,3,4,5]
slice = scores[2..4]
slice[0] = 999
puts scores
答案是 [1, 2, 3, 4, 5]
。那是因为 slice
是一个新数组,并且复制了原有的值。现在,考虑 Go 中的情况:
scores := []int{1,2,3,4,5}
slice := scores[2:4]
slice[0] = 999
fmt.Println(scores)
输出是 [1, 2, 999, 4, 5]
。
这改变了你编码的方式。例如,许多函数采用一个位置参数。JavaScript 中,如果你想去找到字符串中前五个字符后面的第一个空格(当然,在Go中切片也可以用于字符串),我们会这样写:
haystack = "the spice must flow";
console.log(haystack.indexOf(" ", 5));
在 Go 中,我们这样使用切片:
strings.Index(haystack[5:], " ")
我们可以从上面的例子中看到,[X:]
是 从 X 到结尾 的简写,然而 [:X]
是 从开始到 X 的简写。不像其他的语言,Go 不支持负数索引。如果我们想要切片中除了最后一个元素的所有值,可以这样写:
scores := []int{1, 2, 3, 4, 5}
scores = scores[:len(scores)-1]
上面是从未排序的切片中移除元素的有效方法的开始:
func main() {
scores := []int{1, 2, 3, 4, 5}
scores = removeAtIndex(scores, 2)
fmt.Println(scores) // [1 2 5 4]
}
// 不会保持顺序
func removeAtIndex(source []int, index int) []int {
lastIndex := len(source) - 1
// 交换最后一个值和想去移除的值
source[index], source[lastIndex] = source[lastIndex], source[index]
return source[:lastIndex]
}
最后,我们已经了解了切片,我们再看另一个通用的内建函数:copy
。正常情况下,将值从一个数组复制到另一个数组的方法有5个参数,source
, sourceStart
,count
, destination
和 destinationStart
。使用切片,我们仅仅需要两个:
import (
"fmt"
"math/rand"
"sort"
)
func main() {
scores := make([]int, 100)
for i := 0; i < 100; i++ {
scores[i] = int(rand.Int31n(1000))
}
sort.Ints(scores)
worst := make([]int, 5)
copy(worst, scores[:5])
fmt.Println(worst)
}
花点时间试试上面的代码,并尝试改动。去看看如果你这么做 copy(worst[2:4], scores[:5])
或者复制多于或少于 5
个值给 worst
会发什么?