> For the complete documentation index, see [llms.txt](https://sunwenfei.gitbook.io/sunwenfei/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sunwenfei.gitbook.io/sunwenfei/golang/golang-ji-chu-jiao-cheng/shu-zu-qie-pian-bian-can-han-shu/qie-pian-slice.md).

# 切片(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`就不会被继续引用，因此其占用的底层数组就会得以释放。
