# 切片(Slice)

原文是将数组和切换放在同一节介绍，我个人觉得篇幅太长因此将切片(Slice)单独拆成一节。 切片是在数组的基础上包装出来的一个数据类型，当时更加方便、灵活。与数组不同，切片是引用类型(引用的仍是底层数组)。中文**切片**翻译的可能有点生硬，但是也没其他更好的词，后面我会交替着用中文**切片**和英文**slice**。

#### 声明切片

slice的声明方式为`[]T`，跟数组的声明方式仅有一个区别，就是方括号内没有`n`或`...`。

```go
package main

import "fmt"

func main() {  
    a := [5]int{76, 77, 78, 79, 80}
    var b []int = a[1:4] //creates a slice from a[1] to a[3]
    fmt.Println(b)
}
```

[Go Playground在线运行](https://play.golang.org/p/KqwM2cbGgcg)

`a[start:end]`用来基于数组`a`创建一个切片，切取的元素范围为`start`到`end -1`。因此`a[1:4]`切取了数组`a`的索引为`1`到`3`的元素，然后赋值给切片元素`b`。因此切片`b`的元素为`[77 78 79]`。这里我们声明切片`b`也可以使用简写形式：`b := a[1:4]`。

我们也可以通过字面量来声明并初始化切片：

```go
package main

import "fmt"

func main() {  
    c := []int{6, 7, 8} //creates and array and returns a slice reference
    fmt.Println(c)
}
```

[Go Playground在线运行](https://play.golang.org/p/Ziu54CXrpWL) 执行结果如下：`[6 7 8]`。

#### 修改切片

切片仅仅是在数组的基础上包装出来的一个数据类型，修改切片元素时本质上修改的还是底层的数组。

```go
package main

import "fmt"

func main() {  
    darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
    dslice := darr[2:5]
    fmt.Println("array before",darr)
    for i := range dslice {
        dslice[i]++
    }
    fmt.Println("array after",darr) 
}
```

[Go Playground在线运行](https://play.golang.org/p/QiqfXbFQvDo) 这里的切片`dslice`是基于数组`darr`创建的，然后更新了`dslice`的值，打印结果如下：

```go
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 78 67 69 59]
```

可以看出我们修改切片的值归根结底还是修改的底层数组的值。

基于同一个数组创建可以创建多个slice，其中通过任何一个slice改变元素，会影响到其他slice和底层的数组。

```go
package main

import "fmt"

func main() {  
    numa := [3]int{78, 79 ,80}
    nums1 := numa[:] //creates a slice which contains all elements of the array
    nums2 := numa[:]
    fmt.Println("array before change",numa)
    nums1[0] = 100
    fmt.Println("array after modification to slice nums1", numa, nums1, nums2)
    nums2[1] = 101
    fmt.Println("array after modification to slice nums2", numa, nums1, nums2)
    numa[0] = 200
    fmt.Println("array after modification to array numa", numa, nums1, nums2)
}
```

[Go Playground在线运行](https://play.golang.org/p/4mPEb9FS8Uy) 这里的`numa[:]`省去了起止索引，这时会使用默认的起止索引值，`0`和`len(numa)`，也就是说会切取数组的所有元素。切片`nums1`和`nums2`共享一个底层数组`numa`。执行结果如下：

```
array before change [78 79 80]
array after modification to slice nums1 [100 79 80] [100 79 80] [100 79 80]
array after modification to slice nums2 [100 101 80] [100 101 80] [100 101 80]
array after modification to array numa [200 101 80] [200 101 80] [200 101 80]
```

可以看出，由于切片`nums1`和`nums2`底层共享数组`numa`，因此这两个切片，任何一个改变底层的元素都会反应到另一个切片上面(本质上都是改变的数组的元素)。不过这种共享一个数组的特性最好不要用，因为一个切片在容量吃紧时会重新开辟新的底层数组，所以本来共享的好好的，说不定啥时候就翻脸不认人了，这点下面马上会介绍。

#### 切片的长度和容量

* **切片长度**：切片包含的元素个数，通过`len`函数获取。
* **切片容量**：底层数组从切片**切取起始索引**开始到**数组末尾**的长度，通过`cap`函数获取。

我们写个例子来理解下这点。

```go
package main

import "fmt"

func main() {
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice))
}
```

[Go Playground在线运行](https://play.golang.org/p/GL2QAmI3lzD) 这里切片`fruitslice`切取了数组中索引值为`[1, 3)`的元素，因此切片长度为2。由于切片从索引`1`开始切取，从该索引为止到数组末尾长度为6，因此切片容量为6。输出如下：

```
length of slice 2 capacity 6
```

我们不仅可以在一个数组上切片，还可以再一个切片的基础上再次切片，但是切片切取上限时切片的当前容量。

```go
package main

import "fmt"

func main() {  
    fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
    fruitslice := fruitarray[1:3]
    fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6
    fruitslice = fruitslice[:cap(fruitslice)] //re-slicing furitslice till its capacity
    fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))
    fmt.Println(fruitslice)
}
```

[Go Playground在线运行](https://play.golang.org/p/hwvLBaYKb5h) 这里切片`fruitslice`按最大容量重新切片。输出结果如下：

```
length of slice 2 capacity 6
After re-slicing length is 6 and capacity is 6
[orange grape mango water melon pine apple chikoo]
```

#### 追加元素

数组的长度固定不可变，因此不太灵活。而切片是动态长度可变的，可以通过`append`函数不断的给切片添加元素。`append`函数的定义如下：

```go
func append(s []T, x ...T) []T
```

这里的`x ...T`表示该函数接收可变数量的参数(这个主题我们下一节[可变参数](https://sunwenfei.gitbook.io/sunwenfei/golang/golang-ji-chu-jiao-cheng/shu-zu-qie-pian-ke-bian-can-shu/ke-bian-can-shu)会详细讨论)。

现在你估计会问，既然数组长度是固定不可变的，切片底层也是基于数组实现的，那么切片怎么做到动态长度可变的呢？

答案其实很简单，跟某些其他编程语言实现的机制类似，Golang中的切片会在底层数组长度吃紧时重新开辟新的、更大的数组，然后将原数组中的所有元素拷贝进来。这点可以通过一个例子来说明：

```go
package main

import "fmt"

func main() {  
    cars := []string{"Ferrari", "Honda", "Ford"}
    fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) //capacity of cars is 3
    cars = append(cars, "Toyota")
    fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) //capacity of cars is doubled to 6
}
```

[Go Playground在线运行](https://play.golang.org/p/Lj987LOsMv4) 这里切片`cars`初始容量是3，接着我们添加了一个新元素，然后该切片底层重新开辟了新数组，容量翻了一倍，达到了6。输出如下：

```
cars: [Ferrari Honda Ford] has old length 3 and capacity 3
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6
```

现在我们再次看一下前面数组共享的例子：切片`nums1`修改元素会反应到切片`nums2`和底层的数组`numa`上，这有一个前提就是必须是共享一个底层数组，如果切片`nums1`由于不停的`append`元素导致底层数组更换，就不再会跟`nums2`共享底层数组`numa`，也就不会再互相影响了：

```go
package main

import "fmt"

func main() {  
    numa := [3]int{78, 79 ,80}
    nums1 := numa[:] //creates a slice which contains all elements of the array
    nums2 := numa[:]
    fmt.Println("array before change",numa)
    nums1[0] = 100
    fmt.Println("array after modification to slice nums1", numa, nums1, nums2)
    nums2[1] = 101
    fmt.Println("array after modification to slice nums2", numa, nums1, nums2)
    numa[0] = 200
    fmt.Println("array after modification to array numa", numa, nums1, nums2)
    nums1 = append(nums1, 36)
    nums3 := nums1[:]
    nums1[0] = 300
    fmt.Println("array after modification to slice nums1", numa, nums1, nums2, nums3)
    nums1 = append(nums1, 37)
    nums1[0] = 400
    fmt.Println("array after modification to slice nums1", numa, nums1, nums2, nums3)
    nums1 = append(nums1, 38, 39, 40, 50, 60, 70, 80, 90, 100, 101, 102)
    nums1[0] = 500
    fmt.Println("array after modification to slice nums1", numa, nums1, nums2, nums3)
}
```

[Go Playground在线运行](https://play.golang.org/p/BccpEG6ylTQ)

**切片**类型的零值为`nil`，称为空切片。空切片的长度和容量均为0。切片是动态的，即使是空切片也可以动态添加新元素：

```go
package main

import "fmt"

func main() {  
    var names []string //zero value of a slice is nil
    if names == nil {
        fmt.Println("slice is nil going to append")
        names = append(names, "John", "Sebastian", "Vinay")
        fmt.Println("names contents:", names)
        fmt.Println("names capacity:", cap(names))
        names = append(names, "Tom")
        fmt.Println("names capacity:", cap(names))
        names = append(names, "Joy")
        fmt.Println("names capacity:", cap(names))
    }
}
```

[Go Playground在线运行](https://play.golang.org/p/Mdi6Ogk8Ukm) 这里切片`names`声明时未初始化，值为`nil`。该程序执行结果如下：

```go
slice is nil going to append
names contents: [John Sebastian Vinay]
names capacity: 4
names capacity: 4
names capacity: 8
```

我们可以借助`append`函数的可变参数(`x ...T`)，将一个切片所有元素追加到另外一个切片中：

```go
package main

import "fmt"

func main() {  
    veggies := []string{"potatoes","tomatoes","brinjal"}
    fruits := []string{"oranges","apples"}
    food := append(veggies, fruits...)
    fmt.Println("food:",food)
}
```

[Go Playground在线运行](https://play.golang.org/p/znJctOPDYoR) 这里，我们将切片`fruits`中的所有元素按顺序添加进切片`food`中。执行结果如下：

```
food: [potatoes tomatoes brinjal oranges apples]
```

#### 使用`make`声明切片

可以使用`make`函数来声明切片，语法如下：

```go
func make([]T, len, cap) []T
```

其中第三个参数`cap`非必填，默认值为跟`len`相同。`make`函数原理就是创建一个数组，并基于该数组切出一个切片返回。

```go
package main

import "fmt"

func main() {  
    i := make([]int, 5, 5)
    fmt.Println(i)
}
```

[Go Playground在线运行](https://play.golang.org/p/t7wYE6axDco) 使用`make`创建的切片会自动填充零值，该程序执行结果为`[0 0 0 0 0]`。

#### 切片类型的函数参数

学过**C语言**的应该都听过**值传递**和**引用传递**，不了解的话可以参考[这篇文章](https://gabrieletolomei.wordpress.com/miscellanea/programming-languages/c-cpp/pass-by-value-vs-pass-by-reference/)。这里说的变量传递其实就是将一个变量赋值给另外一个变量，包括函数传参。

值传递是说变量赋值时直接传递变量的值，而不是变量的引用(或地址)；而引用传递则是传递变量的引用，并不会传递变量本身的值。听起来比较抽象，其实非常简单，试想一下，假如某个数组变量存储了非常非常多的元素，每次传递该变量都将所有的元素考过来考过去效率肯定非常低，但是直接传递该数组变量的引用(或地址)效率就非常高。这里我们仅讨论下值传递和引用传递的不同，这二者并没有绝对的好坏，只不过不同的传递方式适用于不同的场景罢了，比如函数式编程就推崇函数调用时按值传递，函数应该是纯函数，每次以相同的参数去调用一个纯函数应该是幂等的。

Golang中的切片可以按以下结构理解：

```go
type slice struct {
    Length int
    Capacity int
    ZerothElement *byte
}
```

可看出一个切片主要包括三项：长度，容量以及首个切片元素的指针。当将切片作为参数传递个函数进行调用时，虽然本质上仍然是按值传递，但是指针指向的底层数组还是同一个。因此，如果我们在函数内部修改了切片参数的元素，也会影响到原切片。

```go
package main

import "fmt"

func subtactOne(numbers []int) {  
    for i := range numbers {
        numbers[i] -= 2
    }

}
func main() {  
    nos := []int{8, 7, 6}
    fmt.Println("slice before function call", nos)
    subtactOne(nos)                               //function modifies the slice
    fmt.Println("slice after function call", nos) //modifications are visible outside
}
```

[Go Playground在线运行](https://play.golang.org/p/LItyhDZi9QZ) 这里我们在函数`subtactOne`内部将切片中的所有元素减2，这个修改也影响到了函数外的切片。执行结果如下：

```
slice before function call [8 7 6]
slice after function call [6 5 4]
```

注意这一点跟传递数组参数不一样。

#### 多维切片

跟数组类型，切片也支持多维。

```go
package main

import "fmt"


func main() {  
     pls := [][]string {
            {"C", "C++"},
            {"JavaScript"},
            {"Go", "Rust"},
            }
    for _, v1 := range pls {
        for _, v2 := range v1 {
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
}
```

[Go Playground在线运行](https://play.golang.org/p/A4UuaLQ7Ohf) 执行结果如下：

```
C C++ 
JavaScript 
Go Rust
```

#### 内存占用优化

前面已经介绍过了，切片本质上底层还是引用了一个数组。只要切片还在内存中未被回收，其底层的数组也不会被回收(垃圾回收机制)。如果底层的数组非常大可能会影响内存使用效率。例如，如果底层的数组非常大，但是我们关心和操作的仅仅是其中的一小部分，这时整个数组都处于被引用状态，无法被回收。

这种情况下我们可以通过`copy`方法将切片拷贝成一个新的切片，进而达到释放旧切片及其底层数组的目的。`copy`函数语法为`func copy(dst, src []T) int`。

```go
package main

import "fmt"

func countries() []string {  
    countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
    neededCountries := countries[:len(countries)-2]
    countriesCpy := make([]string, len(neededCountries))
    copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
    return countriesCpy
}
func main() {  
    countriesNeeded := countries()
    fmt.Println(countriesNeeded)
}
```

[Go Playground在线运行](https://play.golang.org/p/Nh58mIHk-Od) 这里我们将切片`neededCountries`拷贝到`countriesCpy`。后面`neededCountries`就不会被继续引用，因此其占用的底层数组就会得以释放。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://sunwenfei.gitbook.io/sunwenfei/golang/golang-ji-chu-jiao-cheng/shu-zu-qie-pian-bian-can-han-shu/qie-pian-slice.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
