# 3.2 切片
在go中,你一般很少直接使用数组。相反,你会使用切片。切片是一个轻量级的结构体封装,这个结构体被封装后,代表一个数组的一部分。这里指出了一些创建切片的方式,并指出我们以后会在何时使用这些方式。
第一种方式和我们创建一个数组时有点细微的变化:
`scores := []int{1,4,293,4,9}`
和声明数组不同的是,声明切片不需要在方括号中指定其大小。理解2中不同的方式创建切片,下面使用另外一种方式创建切片,这里使用`make`:
`scores := make([]int, 10)`
我们使用`make`,没有使用`new`,是因为创建一个切片不仅仅是分配一段内存(`new`只能分配一段内存)。需要特别指出的是,我们需要为底层数组分配内存,并且也要初始化这个切片。在上面的例子中,我们初始化了一个长度和容量都是10的切片。长度表示切片的长度,容量表示底层数组的大小。在使用`make`创建切片时,我们可以分别的指定长度和容量大小:
`scores := make([]int, 0, 10)`
上面创建了一个长度为0但是容量为10的切片。(如果你留意过,你会发现`make`和`len`实现了重载。go语言的一些特性会让你有点失望,因为有些特性是没有暴露出来给开发者使用。)
要更好的理解切片长度和容量之间的相互作用,让我们看下面的例子:
```go
func main() {
scores := make([]int, 0, 10)
scores[5] = 9033
fmt.Println(scores)
}
```
上面的例子不能运行。为什么?因为我们创建的切片长度是0。是的,这个底层的数组有10个元素。但是为了访问切片元素,我们需要明确的扩展切片。一种扩展切片的方式是使用`append`:
```go
func main() {
scores := make([]int, 0, 10)
scores = append(scores, 5)
fmt.Println(scores) // 打印:[5]
}
```
但是这改变为了我们之前代码的意图。当往一个长度为0的切片上添加元素时,这个元素会被赋值给切片的第一个元素。不管出于什么原因,那段不能运行的代码想给切片的第6个元素赋值。为了到达这个目的,我们可以再切分一直我们的切片:
```go
func main() {
scores := make([]int, 0, 10)
scores = scores[0:6]
scores[5] = 9033
fmt.Println(scores)
}
```
调整切片的大小上限是多少?这个上限就是切片的容量大小,在上面的例子中,上限是10。你也许会认为,这实际没有解决定长数组的问题。事实证明,`append`比较特殊。如果底层的数组已经达到上限,`append`会重新创建一个更大的数组,并将所有的值复制过去(这就是动态数组的工作原理,例如php、python、ruby和javascript等等)。这就是为什么我们在上面的例子中使用`append`。我们必须将`append`返回的值重新赋予给`scores`变量:如果原始切片没有更多的空间时,`append`可能会创建一个新值。
如果我告诉你go扩展数组使用的是2倍算法(2x algorithm)。你能猜出下面代码将输出什么吗?
```go
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。但是为了容纳20个元素,切片的容量必须扩展3次,分别是10、20和40。
作为最后一个例子,思考一下:
```go
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个元素的切片添加一个值。
最后,这里提供了4种常用的方式去初始化一个切片:
```go
names := []string{"leto", "jessica", "paul"}
checks := make([]bool, 10)
var names []string
scores := make([]int, 0, 20)
```
你该使用哪一个?第一种你不需要太多的说明。但是使用这种方式你得提前知道你想往数组存放的值。
第二种方式在你想往切片的特定位置写入一个值时很有用,例如:
```go
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, len(saiyans))
for index, saiyan := range saiyans {
powers[index] = saiyan.Power
}
return powers
}
```
第三种方式会返回一个空切片,一般和`append`一起使用,此时切片的元素数量是未知的。
最后一种方式可以让我们指定切片的初始容量。当我们大概知道需要多少元素时很有用。即使你知道元素的个数,`append`也能被使用,这主要取决于个人喜好:
```go
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`,下面代码将输出什么?
```go
scores = [1,2,3,4,5]
slice = scores[2..4]
slice[0] = 999
puts scores
```
答案是`[1, 2, 3, 4, 5]`。因为切片`slice`是由值拷贝组成的一个全新数组。现在,同等情况下看go:
```go
scores := []int{1,2,3,4,5}
slice := scores[2:4]
slice[0] = 999
fmt.Println(scores)
```
输出是`[1, 2, 999, 4, 5]`。
这会如何改变你的代码。例如,很多函数需要一个位置参数。在javascript中,如你我们想在前五个字符后查找一个字符串中的第一个空白符(对,切片也当字符串处理),我们可以这样写:
```javascript
haystack = "the spice must flow";
console.log(haystack.indexOf(" ", 5));
```
在go中,我们使用切片:
`strings.Index(haystack[5:], " ")`
从上面例子中,我们可以看出`[X:]`表示`X`到结尾的一种缩写。而`[:X]`是开始到`X`的一种缩写。不像其他语言,go不支持负值索引。如果我们想要切片所有元素,但除了最后一个,我们可以这样写:
```go
scores := []int{1, 2, 3, 4, 5}
scores = scores[:len(scores)-1]
```
上述是一种从一个乱序的切片中去除一个值的有效方法。
```go
func main() {
scores := []int{1, 2, 3, 4, 5}
scores = removeAtIndex(scores, 2)
fmt.Println(scores)
}
func removeAtIndex(source []int, index int) []int {
lastIndex := len(source) - 1
//swap the last value and the value we want to remove
source[index], source[lastIndex] = source[lastIndex], source[index]
return source[:lastIndex]
}
```
最后,我们已经学习了切片,我们来学习一下另外一个常用的内置函数:`copy`。`copy`是众多函数中重点显示出切片如何改变我们代码方式的函数之一。正常情况下,拷贝一个数组到另外一个数组的方法需要5个参数:`source`,`sourceStart`,`count`,`destination`和`destinationStart`。但是在切片中,我们只需要2个参数:
```go
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`。
## 链接
- 关于本书
- 引言
- 准备工作
- 安装开发环境
- 开始使用Go
- 创建一个Go模块
- 第1章:基础知识
- 1.1 编译
- 1.2 静态类型
- 1.3 类c语法
- 1.4 垃圾回收
- 1.5 运行go代码
- 1.6 导入包
- 1.7 变量和声明
- 1.8 函数声明
- 1.9 继续之前
- 第2章:语法学习
- 2.1 声明和初始化
- 2.2 结构体上的函数
- 2.3 构造函数
- 2.4 new
- 2.5 结构体字段
- 2.6 组合
- 2.7 指针类型和值类型
- 2.8 继续之前
- 第3章:复杂类型
- 3.1 数组
- 3.2 切片
- 3.3 映射
- 3.4 指针类型和值类型
- 3.5 继续之前
- 第4章:面向对象
- 4.1 包
- 4.2 接口
- 4.3 继续之前
- 第5章:综合知识
- 5.1 错误处理
- 5.2 defer
- 5.3 go语言风格
- 5.4 初始化的if
- 5.5 空接口和转换
- 5.6 字符串和字节数组
- 5.7 函数类型
- 5.8 内存分配
- 第6章:高并发
- 6.1 go协程
- 6.2 同步
- 6.3 通道
- 6.4 继续之前
- 第7章:工具库
- 7.1 类型转换
- 7.2 时间操作
- 第8章:程序测试
- 单元测试
- 性能测试
- 第9章:简单实例
- 内存分配
- 第10章:项目实战
- 结论
- 附录