diff --git a/chap-21-Slices/chapter_21.md b/chap-21-Slices/chapter_21.md new file mode 100644 index 0000000000000000000000000000000000000000..0d3708d53d365d6a610cfbcf7b6b03d5c8a2e643 --- /dev/null +++ b/chap-21-Slices/chapter_21.md @@ -0,0 +1,793 @@ +## Chapter 21: 切片(Slices) +![Slices](./imgs/slice.786a106d.jpg) + +## 1 你会在本章学到什么 +* 什么是切片 +* 如何创建一个切片 +* 如何遍历一个切片 +* 如何长间一个多维切片 +* 如何将一个元素放到切片的指定索引处 +* 如何使用append 和 copy + +## 2 覆盖的技术概念 +* 切片 +* 长度 +* 容量 +* 数组 +* 指针 +* 可变参数函数 + +## 3 定义 +切片是一组具有**相同类型**元素的**可增长集合**,它是可增长的因为你在编译阶段指定切片的大小;你可以在执行阶段增加元素。当你在程序执行阶段为切片添加元素时,我们就说切片会增长。 + +切片的类型记作`[]T` T就是切片中的元素类型。 + +例如:`[]int`就是一组整型的切片。 + +注意我们不要像写数组时那样指定切片大小(请查看 [chap:Arrays]() 一章)。在方括号内,我们不指定任何长度。0值的切片是nil。 + +## 4 创建新的切片 +``` +// slices/creation/main.go +package main + +func main() { + s := make([]int, 3) + s[0] = 12 + s[2] = 3 +} +``` +这里我们定义了一个类型为`[]int`的整型切片变量`s`,之后我们可以填充切片。我们定义了索引为0和2的元素。 +``` +s2 = []int{10,12} +``` +我们又定义了一个整型的切片s2,并且我们设置了前两个元素的值 + +## 5 一个数组,一个指向的数组指针,或者一个切片的切片操作 +slice 指的是某种东西的一部分。比如说:一切片芝士说的不是完整的芝士,只是它的一部分。在GO +里,你可以进行相同的切片操作: +* 一个数组 +* 一个指向的数组指针 +* 一个切片 +这种操作(称之为切开)的结果产生一个切片,对元素进行切开,你可以用如下这种语法 +``` +s:=e[low:high] +``` +low 和 high 会帮助你从`e`中截取元素区间 +### 5.0.1 例1 +``` +// slices/slicing-array/main.go +package main + +import "fmt" + +func main() { + customers := [4]string{"John Doe", "Helmuth Verein", "Dany Beril", "Oliver Lump"} + // 切一个数组 + customersSlice := customers[0:1] + fmt.Println(customersSlice) +} +``` +这段程序将会输出: +``` +[John Doe] +``` + +我们有一个字符串数组`customers`,这个数组包含4个元素。 + +我们从这个数组创建一个切片。`customers[0:1]` 将会创建一个切片包含这个数组中的元素从索引为0到索引为1-1=0的元素。换句话说,我们取的数组中的第一个元素。 + +### 5.0.2 例2 +``` +customersSlice2 := customers[2:4] +fmt.Println(customersSlice2) +``` +这段程序将会输出: + +``` +[Dany Beril Oliver Lump] +``` +我们取的元素是从索引2 到索引4-1 = 3 + +### 5.0.3 谨记 +当你写成: +``` +s := e[low:high] +``` +你必须从high 减去1 才能得到获取e中的最高元素索引。 +另一种记忆方式就是我们获取的元素是从low 到 high 左闭右开区间 + +## 6 切片操作是复制了数据么? +不!这里有个例子: +``` +// slices/slicing-copy/main.go +package main + +import "fmt" + +func main() { + customers := [4]string{"John Doe", "Helmuth Verein", "Dany Beril", "Oliver Lump"} + customersSlice := customers[0:1] + fmt.Println(customersSlice) + // 修改了原始数组 + customers[0] = "John Doe Modified" + fmt.Println("After modification of original array") + fmt.Println(customersSlice) +} +``` +这段程序将会输出: +``` +[John Doe] +After modification of the original array +[John Doe Modified] +``` +当我们创建了一个切片`customersSlice`数据没有复制,取而代之的是**一个原始数据的引用**,切片中索引为0的值将会改变。 +## 7 字符串切片操作 +你也可以对字符串进行切片操作!操作的结果是另一个字符串: +``` +hotelName := "Go Dev Hotel" +s := hotelName[0:6] +fmt.Println(s) +``` +这段程序将会输出:`Go Dev`。字符串在Go 里面是**不可更改的**。一旦创建并且保存到了内存中,你就不能修改这个字符串了。让我们来看一个例子: +``` +// slices/slicing-string/main.go +package main + +import "fmt" + +func main() { + hotelName := "Go Dev Hotel" + s := hotelName[0:6] + fmt.Println(s) + hotelName = "Java Dev Hotel" + fmt.Println(s) +} +``` +在这个例子中,我们首先创建了一个字符串(`hotelName`)然后进行切片操作。切片命名为`s`。然后我们修改`hotelName`的值。 +我们以为`s`会被修改,但实际上并没有。这段程序将会输出: +``` +Go Dev +Go Dev +``` +## 8 长度 +长度是指**切片中元素的数量**,内置函数`len`返回的是切片的长度: +``` +vatRates := []float64{4.65, 4, 15, 20} +fmt.Printf("length of slice vatRates is %d", len(vatRates)) +``` +## 9 内部实现 +在内部切片是一个包含了**指向数组的指针**的结构。 +* 当我们创建了一个切片,Go会创建一个数组。当然你访问不到这个数组,这个数组只有**内部可见**。 +* 当对数组进行切片操作,Go 会将一个指针指向这个现有的数组。 + +![Slices](./imgs/slice_internal_rep.2d12e3a0.png) + +切片的内部实现[fig:Slice-internal-representation] + +切片是由三个元素组成: +1. 一个指向底层数组的指针。这个指针在切片开始时指向底层数组。它指向了**切片的第一个元素** +2. 切片的长度(uint 类型) +3. 容量(uint 类型) (请看下一小节) +当我们在程序中创建了一个切片,一个数组也会被创建。这个数组包含了这个切片的元素。在Go内部会存储一个指向这个数组第一个元素的指针(请看下图) +![Slices](./imgs/slice_inernal_example.6aa32199.png) +切片内部实现[fig:Slice-internal-example] +切片也可以有一个现有数组进行切片操作后创建。这种情况下,切片的容量不等于它的长度 +![Slices](./imgs/slice_internal_when_slicing.0fff636d.png) +当一个数组进行切片操作时的切片内部实现[fig:Slice-internal-example-when-array-sliced] + +## 10 容量 +容量是一个无符号的整型。Go文档它表示“底层数组中为其分配空间的元素个数”: https://golang.org/ref/spec#Length_and_capacity 想要得到切片的容量,可以使用内置函数`cap(slice)`。 + +让我们用一个例子来理解容量的概念: +``` +names := [4]string{"John", "Bob", "Claire", "Nik"} +mySlice := names[1:3] +fmt.Println("length:", len(mySlice)) +fmt.Println("capacity:", cap(mySlice)) +``` +这段程序将会输出: +``` +length: 2 +capacity: 3 +``` +我们通过对数组`names`进行切片操作创建了名为 `mySlice`的切片。我们得到了数组索引为1到索引为2(3-1)之间的所有数据。`mySlice`的长度为2(其中有两个元素)。但是容量却是3。这里为底层数组`names`分配了3个元素的空间。 +## 11 长度和容量的关系 +**容量永远大于等于长度**。如果容量比长度小那是没有意义的。想象一下一个长度为3容量为2的数组。我们只能在底层数组存储2个元素! +## 12 切片(slices)作为函数入参 +切片作为一个函数的入参时可以修改底层数组。为什么?因为在切片内部是指针指向底层数组。来看一个例子: +``` +// slices/as-fct-parameters/main.go +package main + +import "fmt" + +func main() { + s := []int{1, 2, 3} + multiply(s, 2) + fmt.Println(s) + //[2 4 6] +} + +func multiply(slice []int, factor int) { + for i := 0; i < len(slice); i++ { + slice[i] = slice[i] * factor + } +} +``` +我们定义了个切片名为`s`(由整型组成)。我们创建了一个函数`multiply`由整型切片和 `factor`(int)作为入参。函数的目的是将切片中的每个元素乘以`factor`。当`s`被创建后Go内部会创建一个包含值为1,2和3的数组。还会创建一个指向这个数组的指针。 +当我们把`s`作为参数传递给`multiply`时,底层数组也会被修改了。**切片天生就是指针**。 +### 12.1 共性错误 +* 当你把一个切片传递给函数,函数可以通过修改底层数组的值来修改切片 +* 当你在函数内部向这个切片增加元素并且你的切片达到了最大容量,运行时会分配一个新的底层数组。 +* 这种行为很常见;然而,在函数执行的末尾,添加到切片的元素可能不会如你所愿的那样展示出来。 +#### 12.1.1 例子 +``` +// slices/common-error-function/main.go +package main + +import "fmt" + +func main() { + languages := []string{"Java", "PHP", "C"} + fmt.Println("Capacity :", cap(languages)) + // Capacity : 3 + + // call function + addGo(languages) + + fmt.Println("Capacity :", cap(languages)) + // Capacity : 3 + fmt.Println(languages) + // [Java PHP C] + // 什么 ! Go 哪去了 ????? +} + +func addGo(languages []string) { + languages = append(languages, "Go") + fmt.Println("in function, capacity", cap(languages)) +} +``` +#### 12.1.2 解释 +* 我们创建了一个函数 addGo ,它会给字符串类型的切片添加一个元素。 +* 在main函数里,我们初始化了一个字符串类型的切片叫languages。 +* 我们用三个元素初始化这个切片。它的容量和长度都是3。 +* 之后我们调用了函数 addGo。调用结束后,我们打印这个切片,没有元素被添加过。 + +这个函数里发生了什么? +* 当函数被调用,运行时创建了一个切片的复制。 +* 由于容量超出,一个新的底层数组在函数中被分配。 +* 由切片持有的底层数组的引用被改变了。 +* 但是,它只改变了切片的复制。 +* 当函数返回,切片的复制被销毁了。切片languages仍然是旧底层数组的引用。 +#### 12.1.3 如何避免 +* addGo 应该接收一个字符串切片的指针作为入参。 +* 在这种情况下切片不会被复制。 +* 对底层数组的引用在原始切片中被更新。 +``` +// slices/common-error-function-fix/main.go +package main + +import "fmt" + +func main() { + languages := []string{"Java", "PHP", "C"} + fmt.Println("Capacity :", cap(languages)) + // Capacity : 3 + + // 调用函数 + addGoFixed(&languages) + + fmt.Println("Capacity :", cap(languages)) + // Capacity : 6 + fmt.Println(languages) + // [Java PHP C Go] +} + +func addGoFixed(languages *[]string) { + *languages = append(*languages, "Go") +} +``` +* `*`表示“跟随这个地址”,是取值运算符(如果不够清楚,看一下指针的相关章节) +## 13 内置函数make +通过内置的make,你指定长度和容量来创建切片,`make`会返回一个新的切片。你必须提供你想要放到切片里的元素类型以及长度和容量。注意容量是可选的。如果不提供,容量将会等于长度。 +``` +test := make([]int,2,10) +``` +切片`test`的长度是2容量是10。小心容量不能小于长度;否则,无法编译通过! +## 14 内置函数copy +copy 是一个很方便的函数,它允许你复制一个切片(称为源切片)的所有数据到另一个切片(称为目标切片)。copy 函数有两个入参。内置函数会复制第二个切片的所有元素到第一个切片。我们成第一个切片为目标切片,第二个切片为源切片。 + +函数的的返回值是一个整型,代表成功复制的元素数量。 +``` +func copy(dst, src []Type) int +``` +注意复制的元素数量取决于源切片长度和目标切片长度的最小值。 + +举个例子,一个切片包含4个元素,源切片有2个。复制的元素将会是2个。如果目标切片的长度是1,那么复制的元素就只有1个。内置的copy不会增长目标切片的长度。如果你想要一个相通的切片,你必须提供一个长度跟源切片一样的目标切片。 + +![Slices](./imgs/slices_copy.2eb74fde.png) +copy的使用 + +这里有一个使用内置copy的例子: +``` +// slices/copy-builtin/main.go +package main + +import "fmt" + +func main() { + // 目标 > 源 + a := []int{10, 20, 30, 40} + b := []int{1, 1, 1, 1, 1} + copy(b, a) + fmt.Printf("a: %v - b : %v\n", a, b) + // 输出 : a: [10 20 30 40] - b : [10 20 30 40 1] + + // 源 > 目标 + a = []int{10, 20, 30, 40} + b = []int{1, 1} + copy(b, a) + fmt.Printf("a: %v - b : %v\n", a, b) + // 输出 : a: [10 20 30 40] - b : [10 20] + + // 源 = 目标 + a = []int{10, 20, 30, 40} + b = make([]int, 4) + copy(b, a) + fmt.Printf("a: %v - b : %v\n", a, b) + // 输出 : a: [10 20 30 40] - b : [10 20 30 40] +} +``` +想要记住copy 的第一个参数是目标切片另外一个是源,有点困难。 + +这里有一个方便记忆的小技巧:参数是按照字母表的顺序,从D(目标) 到S(源)。 +## 15 内置函数append +append 函数会增加一个或多个元素到切片末尾。 +![Slices](./imgs/append.96e05fd1.png) +内置append +append 接收的第一个参数是切片,然后增加一个或几个元素到切片上。我们说append 是可变参数函数。 + +当一个函数拥有可变数量的参数,就把这个函数被称为是**可变参数**。函数的定义不会修改参数的个数。在函数的签名里你可以通过找三个点“...”来察觉到。 +``` +func append(slice []Type, elems ...Type) []Type +``` +这个函数的参数是一个切片和几个类型是`Type`的元素,那些点表示我们可以**为第二个参数elems提供几个值**。append 返回修改后的切片。让我们看一个使用的例子: +``` +// slices/append/main.go +package main + +import "fmt" + +func main() { + a := []int{10, 20, 30, 40} + a = append(a, 50) + fmt.Println(a) + // [10 20 30 40 50] + +} +``` +这里我们首先创建了一个切片`a`然后我们用append 增加了一个整型`50`。注意`append`函数会**返回修改后的**`a`。因此我们必须将修改后的版本重新赋值给变量`a`。 + +在代码的最后,我们打印了`a`,得到了一个带有新元素的切片。append函数自动的调整了原始切片的大小。如果底层切片的容量不足,append函数将会分配一个新的。 + +## 16 切片是如何增长的 +通过内置函数`append`我们可以增加更多的数据到切片的末尾。在内部当我们为切片增加了一个值,我们在底层数组设置了一个值。底层数组有个明确的大小。如果数组没有空间了,Go会创建一个充足空间的数组。Go会从之前的数组复制所有的元素到新数组上。让我们来看一个例子: +``` +s := []uint{10, 20, 30, 40} +fmt.Printf("Length : %d - Capacity : %d\n", len(s), cap(s)) +s = append(s, 50) +fmt.Printf("Length : %d - Capacity : %d\n", len(s), cap(s)) +s = append(s, 60) +fmt.Printf("Length : %d - Capacity : %d\n", len(s), cap(s)) +s = append(s, 70) +fmt.Printf("Length : %d - Capacity : %d\n", len(s), cap(s)) +s = append(s, 80) +fmt.Printf("Length : %d - Capacity : %d\n", len(s), cap(s)) +s = append(s, 90) +fmt.Printf("Length : %d - Capacity : %d\n", len(s), cap(s)) +``` +在这段程序中,我们创建了一个含有4个初始值的切片,然后我们添加新的元素到切片上。我们记录了每一步之后的切片长度和容量。程序最后输出: +``` +Length : 4 - Capacity : 4 +Length : 5 - Capacity : 8 +Length : 6 - Capacity : 8 +Length : 7 - Capacity : 8 +Length : 8 - Capacity : 8 +Length : 9 - Capacity : 16 +``` +* 首先,我们有4个元素和一个4个元素底层数组。 +* 当我们添加一个元素到切片上,容量增长到了8。 + * 这代表了一个新的具有8个元素的底层数组被创建了。 + * 存在于旧数组的元素全部复制到了新数组中。 + +在下图中我们可以切片是怎么增长的 +![Slices](./imgs/slice_growing.53418fa9.png) +切片的增长[fig:Slice-growing] + +#####16.0.0.1 性能影响 +当底层数组已满,一个新的数组被创建,你的数据被复制到新的数组上。这个操作会影响你程序的性能。让我们看一个例子: +``` +func grow1() { + s := []uint{10, 20, 30, 40} + s = append(s, 50) + s = append(s, 60) + s = append(s, 70) + s = append(s, 80) + s = append(s, 90) +} + +func grow2() { + s := make([]uint, 9) + s = append(s, 10, 20, 30, 40) + s = append(s, 50) + s = append(s, 60) + s = append(s, 70) + s = append(s, 80) + s = append(s, 90) +} +``` +函数`grow2`比函数`grow1`快,在`grow2`我们用了内置函数`make`。我们明确的说明我们需要长度为9(因此容量也是9)。运行时会立刻创建一个底层数组。在`grow1`Go 将会创建两个底层数组(并且从一个数组复制所有元素到另一个数组)。这里有一个基准测试(请查看基准测试章节 [chap:Benchmark]())运行在我的Mac Book Pro(2,2GHZ 4核Inter Core i7,16GB 1600 MHz DDR3)上。`grow1`每次运行平均94.8纳秒,`grow2`每次运行平均58.7纳秒 +## 17 切片元素索引 +切片中的元素是有索引标记的,索引从**0**开始,在计算机科学世界,我们从0开始计数。这对初学者来说是经常出错的地方之一。请一定要牢记。 +![Slices](./imgs/slice_indexing.c27b6758.png) +一个切片的索引 +## 18 通过元素的索引访问元素 +所以如果你想要从切片中访问一个索引为3的元素,你可以用下面这段代码: +``` +elementAtIndex3 := a[3] +``` +你必须确保这个索引是有元素存在的。否则,你会遇到一个**运行时异常**。例如,下面这段程序会编译通过(编译器不会检查索引越界),看这个例子: +``` +// slices/access-element-index/main.go +package main + +import "fmt" + +func main() { + a := []int{10, 20, 30, 40} + fmt.Println(a[8]) +} +``` +这段程序会编译通过。我们想访问索引为8的元素。但是这个元素并不存在。因此程序会引发异常: +``` +$ go run main.go +panic: runtime error: index out of range +``` +如果你想要访问数组中越界的元素,程序不会编译通过。数组的长度在编译阶段可以知道。切片的长度在程序的执行阶段是可以增长的。 +## 19 遍历切片的元素 +想要遍历切片中的元素,你可以用for循环: +``` +names := []string{"John", "Bob", "Claire", "Nik"} +for i, name := range names { + fmt.Println("Element at index", i, "=", name) +} +``` +程序将会输出: +``` +Element at index 0 = John +Element at index 1 = Bob +Element at index 2 = Claire +Element at index 3 = Nik +``` +在每次迭代中: +* `i`的值等于索引遍历的值。 +* `name`的值等于切片在索引`i`处的元素的值。 +### 19.1 注意危险:for 循环会修改切片 +想象一下你需要修改切片中的每个元素。for循环的range 表达式会完成这个工作。但是这里有个初学者错误需要避免。 + +``` +// 警告:这是个陷阱 +names := []string{"John", "Bob", "Claire", "Nik"} +for _, name := range names { + name = strings.ToUpper(name) +} +fmt.Println(names) +``` +这段程序将会输出: +``` +[John Bob Claire Nik] +``` +原始的切片并没有被修改。range 表达式会复制每个元素的值到局部变量`name`。`name = strings.ToUpper(name)` 只会修改局部变量。原始的切片names 一点都没动过。 +``` +for i := range names { + names[i] = strings.ToUpper(names[i]) +} +fmt.Println(names) +``` +这段程序将会输出: +``` +[JOHN BOB CLAIRE NIK] +``` +## 20 合并两个切片 +下面一小段代码会合并`b`和`c`: +``` +b := append(b, c...) +``` +注意`b`和`c` 必须是相同类型。 +``` +test := append([]int{10,20}, []int{30,40,50}...) +``` +test变量等同与下面的切片: +``` +[10,20,30,40,50] +``` +## 21 查询切片中的一个元素 +这里就没有相关的内置函数了。 + +你不得不遍历切片每个元素并检查这个元素是否是你找的那个元素。 +``` +names := []string{"John", "Bob", "Claire", "Nik"} +for i, name := range names { + if name == "Claire" { + fmt.Println("Claire found at index", i) + } +} +// 输出 : Claire found at index 2 +``` +## 21 移除指定索引的元素 算法来自 https://github.com/golang/go/wiki/SliceTricks +假定我们有个长度为10的切片: +``` +[1,2,3,4,5,6,7,8,9,10] +``` +我们想去掉索引为8位置上的元素(就是值为9的元素)。算法是创建两个切片。第一个切片包含从索引为0 到索引为7的元素。第二个切片只包含一个元素。在索引为9的元素。 +然后我们合并两个切片。 +![Slices](./imgs/slice_delete_one_element.c56c3354.png) +删除索引为8的元素[fig:Delete-the-element-at-index] + +这里有Go的实现: +``` +a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} +a = append(a[:8], a[9:]...) +``` +首先我们创建一个切片名为`a`。 + +然后我们用内置函数append来连接两个切片: +* `mySlice[:8]`:是`mySlice`简化为**0到7**索引处的元素集合.(记住,“:”后面的索引总是被排除的)就是上图中的B +* `mySlice[9:]`:包括`mySlice`索引9之后的所有元素。上图中的C + +然后我们连接两个切片来获得我们的新切片不包括索引8处的元素! + +下面是算法概括。如果你有一个切片并且你想删除索引为i的元素`i