声明&创建

声明切片

  1. []<元素类型>{元素1, 元素2, …}

创建一个有 3 个整型元素的数组,并返回一个存储在 c 中的切片引用。

  1. c := []int{6, 7, 8}

make创建

内置函数make创建 slice

  1. s1 := make([]int, 5) //长度和容量都是 5
  2. s2 := make([]int, 5, 10) //长度是5,容量是10
  3. fmt.Println(cap(s1),s2)

make函数时,需要传入一个参数,指定切片的长度,例子中我们使用的时5,这时候切片的容量也是5。当然我们也可以单独指定切片的容量。make([]int, 5, 10) 表示长度是5,容量是10。
切片的容量必须>=长度,我们是不能创建长度大于容量的切片的。

  1. s:=make([]int,5,3)
  2. fmt.Println(s)

上面的语句会出现如下错误

  1. len larger than cap in make([]int)

在创建切片时指定了切片长度是5,容量是10,主要我们只能访问5个个元素,剩下的5个元素,需要切片扩充后才可以访问。

  1. func main() {
  2. s:=make([]int,5,10)
  3. for i,v:=range s{
  4. fmt.Printf("index=%d,value=%v\n",i,v)
  5. }
  6. fmt.Println(s[5])
  7. }

上面的程序执行结果是,访问s[5]会出现数组越界panic 错误

  1. go run main.go
  2. index=0,value=0
  3. index=1,value=0
  4. index=2,value=0
  5. index=3,value=0
  6. index=4,value=0
  7. panic: runtime error: index out of range

基于数组或切片创建

基于现有的切片或者数组创建,使用[i:j]这样的操作符即可,表示以i索引开始,到j索引结束,截取原数组或者切片,
创建而成的新切片,新切片的值包含原切片的i索引,但是不包含j索引。注意i和j都不能超过原切片或者数组的索引,i如果省略,默认是0;j如果省略默认是原数组或者切片的长度,

  1. slice :=[]int{1,2,3,4,5}
  2. slice1 := slice[:]
  3. slice2 := slice[0:]
  4. slice3 := slice[:5]
  5. fmt.Println(slice1)
  6. fmt.Println(slice2)
  7. fmt.Println(slice3)

新的切片和原数组或原切片共用的是一个底层数组,所以当修改的时候,底层数组的值就会被改变,所以原切片的值也改变了。

  1. package main
  2. import "fmt"
  3. func main() {
  4. slice := []int{1, 2, 3, 4, 5}
  5. newSlice := slice[1:3]
  6. newSlice[0] = 10
  7. fmt.Println(slice)
  8. fmt.Println(newSlice)
  9. }

上面程序直接结果,修改新切片值 也会对原来切片产生影响,这个需要注意

  1. [1 10 3 4 5]
  2. [10 3]

但是如下将程序改成如下

  1. package main
  2. import "fmt"
  3. func main() {
  4. s := []int{1, 2, 3, 4, 5}
  5. newSlice := s[1:3]
  6. s = append(s,6)
  7. newSlice[0] = 10
  8. fmt.Println(s)
  9. fmt.Println(newSlice)
  10. }

再次执行程序结果如下

  1. [1 2 3 4 5 6]
  2. [10 3]

因为原有的切片发生了扩容 底层数组被重新创建 ,和原来的切片已经没有关系了 ,这个也需要特别注意

切片与数组

slice 源码地址在/runtime/slice.go ,带有 T 类型元素的切片由 []T 表示,其中T代表slice中元素的类型。切片在内部可由一个结构体类型表示,形式如下:

  1. type slice struct {
  2. array unsafe.Pointer
  3. len int
  4. cap int
  5. }

可见一个slice由三个部分构成:指针、长度和容量。其中:

  1. array 指针指向第一个slice元素对应的底层数组元素的地址。
  2. len长度对应slice中元素的数目;
  3. cap容量一般是从slice的开始位置到底层数组的结尾位置。长度值不能超过容量值。

切片可以看做是对数组的封装,每个切片的底层数组结构中一定包含一个数组,切片与数组的关系总结如下:
1.切片不是数组,但是切片底层指向数组
2.切片本身长度是不一定的因此不可以比较,数组是可以的。
3.切片是变长数组的替代方案,可以关联到指向的底层数组的局部或者全部。
4.切片是引用传递(传递指针地址),而数组是值传递(拷贝值)
5.切片可以直接创建,引用其他切片或数组创建
6.如果多个切片指向相同的底层数组,其中一个值的修改会影响所有的切片

切片的修改

切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  7. slice := arr[2:5]
  8. fmt.Println("array before", arr)
  9. for i := range slice {
  10. slice[i]++
  11. }
  12. fmt.Println("array after ", arr)
  13. }

在上述程序的第 9 行,我们根据数组索引 2,3,4 创建一个切片 dslice。for 循环将这些索引中的值逐个递增。当我们使用 for 循环打印数组时,我们可以看到对切片的更改反映在数组中。该程序的输出是

  1. array before [0 1 2 3 4 5 6 7 8 9]
  2. array after [0 1 3 4 5 5 6 7 8 9]

底层数组是可以被多个 slice 同时指向的。当多个切片共用相同的底层数组时,每个切片所做的更改将反映在数组中。因此对一个 slice 的元素进行修改操作会影响到其他 共用底层数组的slice 的。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. array := [4]int{10, 20 ,30, 40}
  7. slice1 := array[:]
  8. slice2 := array[:]
  9. fmt.Println("array before change:", array)
  10. slice1[0] = 60
  11. fmt.Println("array after modification to slice slice1:", array)
  12. slice2[1] = 70
  13. fmt.Println("array after modification to slice slice2:", array)
  14. }

在 9 行中,numa [:] 缺少开始和结束值。开始和结束的默认值分别为 0len (numa)。两个切片 nums1nums2 共享相同的数组。该程序的输出是

  1. array before change: [10 20 30 40]
  2. array after modification to slice slice1: [60 20 30 40]
  3. array after modification to slice slice2: [60 70 30 40]

从输出中可以清楚地看出,当切片共享同一个数组时,每个所做的修改都会反映在数组中。

len(长度)和cap(容量)

s := make([]int, 5)当使用make函数初始化切片的是,如果不指定切片的容量,那么切片的长度就是切片的容量。对于底层数组容量是k的切片s[i:j]来说,长度:j-i,容量:k-i,s[1:3],长度就是3-1=2,容量是5-1=4

  1. func TestSlice(t *testing.T) {
  2. a := []int{1, 2, 3, 4}
  3. b := a[:2]
  4. c := a[2:]
  5. t.Log(a, len(a), cap(a))
  6. t.Log(b, len(b), cap(b))
  7. t.Log(c, len(c), cap(c))
  8. t.Log("append data")
  9. b = append(b, 5)
  10. t.Log(a, len(a), cap(a))
  11. t.Log(b, len(b), cap(b))
  12. t.Log(c, len(c), cap(c))
  13. }

执行结果是:

  1. [1 2 3 4] 4 4
  2. [1 2] 2 4
  3. [3 4] 2 2
  4. append data
  5. [1 2 5 4] 4 4
  6. [1 2 5] 3 4
  7. [5 4] 2 2

切片的长度是切片中的元素数。切片的容量是从创建切片索引开始的底层数组中元素数。

  1. func main() {
  2. var x []int
  3. c := 0
  4. for i := 0; i < 100000; i++ {
  5. x = append(x, i)
  6. if c!= cap(x){
  7. c =cap(x)
  8. fmt.Printf("index=%d cap=%d\n", i+1, c)
  9. }
  10. }
  11. }

执行的结果,容量每次扩容是2倍,当容量达到1024时 会变成1.25,也就是每次按当前大小1/4增长,但是不一定是扩大1.25,会做内存对齐

  1. index=1 cap=1
  2. index=2 cap=2
  3. index=3 cap=4
  4. index=5 cap=8
  5. index=9 cap=16
  6. index=17 cap=32
  7. index=33 cap=64
  8. index=65 cap=128
  9. index=129 cap=256
  10. index=257 cap=512
  11. index=513 cap=1024
  12. index=1025 cap=1280
  13. index=1281 cap=1696
  14. index=1697 cap=2304
  15. index=2305 cap=3072
  16. index=3073 cap=4096
  17. index=4097 cap=5120
  18. index=5121 cap=7168
  19. index=7169 cap=9216
  20. index=9217 cap=12288
  21. index=12289 cap=15360
  22. index=15361 cap=19456
  23. index=19457 cap=24576
  24. index=24577 cap=30720
  25. index=30721 cap=38912
  26. index=38913 cap=49152
  27. index=49153 cap=61440
  28. index=61441 cap=76800
  29. index=76801 cap=96256
  30. index=96257 cap=120832

扩容源码:
image.png
还有一种3个索引的方法,第3个用来限定新切片的容量,其用法为slice[i:j:k]。k不能超过原切片的最大索引值
j-i 表示长度,k-i表示容量

  1. package main
  2. import "fmt"
  3. func main() {
  4. s := []int{1, 2, 3, 4, 5}
  5. newSlice := s[1:2:3]
  6. fmt.Printf("slice lenth=%d,cap=%d\n",len(s),cap(s))
  7. fmt.Printf("newSlice lenth=%d,cap=%d\n",len(newSlice),cap(newSlice))
  8. }

执行结果如下

  1. slice lenth=5,cap=5
  2. newSlice lenth=1,cap=2

增加元素

使用 append 函数可以将新元素追加到切片上。append 函数的定义是

  1. func appends[]Tx ... T)[]T

append可以直接在切片尾部追加元素

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. str := []string{"a", "b", "c"}
  7. fmt.Println("strs:", str, " length:", len(str), "capacity:", cap(str))
  8. str = append(str, "d")
  9. fmt.Println("strs:", str, " length:", len(str), " capacity:", cap(str))
  10. }

在上述程序中,str 的容量最初是 3。在第 10 行,我们给 str 添加了一个新的元素,并把 append(str, "d") 返回的切片赋值给 str。现在 str 的容量翻了一番,变成了 6。

  1. strs: [a b c] length: 3 capacity: 3
  2. strs: [a b c d] length: 4 capacity: 6
  1. func TestSliceCap(t *testing.T) {
  2. a := []int{0}
  3. a = append(a, 0)
  4. b := a[:]
  5. a = append(a, 2)
  6. b = append(b, 1)
  7. t.Log(a[2])
  8. t.Log(b[2])
  9. t.Log(a)
  10. t.Log(b)
  11. // 一样的代码,只是以一个稍大的 slice 开始
  12. c := []int{0, 0}
  13. c = append(c, 0)
  14. d := c[:]
  15. c = append(c, 2)
  16. d = append(d, 1)
  17. t.Log(c[3])
  18. t.Log(d[3])
  19. t.Log(c)
  20. t.Log(d)
  21. }

当切片新长度没有超过切片的容量的时,使用append函数追加数据不会创建新的底层数组,但是会造成新增加的数据覆盖原来底层索引位置的数据,打印结果:

  1. 2
  2. 1
  3. [0 0 2]
  4. [0 0 1]
  5. 1
  6. 1
  7. [0 0 0 1]
  8. [0 0 0 1]

append 增加的元素是插入尾部,如果需要讲元素插入到首部,可以通过一种取巧的方式实现,因为append增加元素的也可以是切片,切片增加切片的方式。

  1. slice := []int{1, 2, 3}
  2. // 将元素4,5,6插入到slice的前面
  3. slice = append([]{4, 5, 6}, slice...)
  4. // 输出:[4 5 6 1 2 3]

nil和空切片

nil slice表示数组不存在,empty slice表示集合为空。对slice为空的判断建议len函数

  1. var s []int //nil值
  2. var t = []int{} //空值
  3. a,_:= json.Marshal(s)
  4. fmt.Println(string(a))
  5. b,_:=json.Marshal(t)
  6. fmt.Println(string(b))

需要注意的是分别对nil和空slice做json序列化是不同的, nil slice会变成null,empty是[]

  1. null
  2. []

切片添加切片

使用 ... 运算符将一个切片添加到另一个切片。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. veggies := []string{"potatoes", "tomatoes", "brinjal"}
  7. fruits := []string{"oranges", "apples"}
  8. food := append(veggies, fruits...)
  9. fmt.Println("food:",food)
  10. }

在上述程序的第 10 行,food 是通过 append(veggies, fruits…) 创建。程序的输出为 food: [potatoes tomatoes brinjal oranges apples]
特别需要注意的是如果新切片的长度未超过源切片的容量,则返回源切片,如果追加后的新切片长度超过源切片的容量,则会返回全新的切片。

  1. func main() {
  2. s1 := []int{1,2,3,4,5}
  3. fmt.Printf("s1:%p %d %d %v\n",s1,len(s1),cap(s1),s1)
  4. s2 :=append(s1,6)
  5. fmt.Printf("s3:%p %d %d %v\n",s2,len(s2),cap(s2),s2)
  6. s3 := s1[0:4]
  7. fmt.Printf("s3:%p %d %d %v\n",s3,len(s3),cap(s3),s3)
  8. s4 := append(s3,6)
  9. fmt.Printf("s4:%p %d %d %v\n",s4,len(s4),cap(s4),s4)
  10. fmt.Printf("s1:%p %d %d %v\n",s1,len(s1),cap(s1),s1)
  11. s5 := append(s4,8)
  12. fmt.Printf("s5:%p %d %d %v\n",s5,len(s5),cap(s5),s5)
  13. }

切片的函数传递

切片包含长度、容量和指向数组第零个元素的指针。当切片传递给函数时,即使它通过值传递,指针变量也将引用相同的底层数组。因此,当切片作为参数传递给函数时,函数内所做的更改也会在函数外可见。

  1. package main
  2. import "fmt"
  3. func main() {
  4. s:= []int{1:1}
  5. fmt.Printf("current slice point:%p\n" ,&s)
  6. fmt.Println("slice before function",s)
  7. modify(s)
  8. fmt.Println("slice after function",s)
  9. }
  10. func modify(s []int) {
  11. fmt.Printf("function slice point:%p\n" ,&s)
  12. s[1] = 2
  13. }

执行上面程序,执行结果如下,两个切片的地址不一样,所以可以确认切片在函数间传递是复制的。而我们修改一个索引的值后,发现原切片的值也被修改了,说明它们共用一个底层数组

  1. current slice point:0xc00000c060
  2. slice before function [0 1]
  3. function slice point:0xc00000c0a0
  4. slice after function [0 2]

多维切片

类似于数组,切片可以有多个维度。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. pls := [][]string {
  7. {"C", "C++"},
  8. {"JavaScript"},
  9. {"Go", "Rust"},
  10. }
  11. for _, v1 := range pls {
  12. for _, v2 := range v1 {
  13. fmt.Printf("%s ", v2)
  14. }
  15. fmt.Printf("\n")
  16. }
  17. }

程序的输出为,

  1. C C++
  2. JavaScript
  3. Go Rust

copy

切片持有对底层数组的引用。只要切片在内存中,数组就不能被垃圾回收。在内存管理方面,这是需要注意的。让我们假设我们有一个非常大的数组,我们只想处理它的一小部分。然后,我们由这个数组创建一个切片,并开始处理切片。这里需要重点注意的是,在切片引用时数组仍然存在内存中。
一种解决方法是使用copy 函数来生成一个切片的副本。这样我们可以使用新的切片,原始数组可以被垃圾回收。

  1. `func copy(dst,src[]T)int`
  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. s1 :=[]int{1,2,3,4,5}
  7. fmt.Println("s1",s1)
  8. s2 := make([]int,len(s1))
  9. fmt.Println("s2",s2)
  10. copy(s2,s1)
  11. fmt.Println("s2",s2)
  12. s3 :=make([]int,len(s1)-2)
  13. copy(s3,s1);
  14. fmt.Println("s3",s3)
  15. s4 :=make([]int,len(s1)-1)
  16. copy(s4[1:3],s1[2:4]);
  17. fmt.Println("s4",s4)
  18. }

打印结果:

  1. s1 [1 2 3 4 5]
  2. s2 [0 0 0 0 0]
  3. s2 [1 2 3 4 5]
  4. s3 [1 2 3]
  5. s4 [0 3 4 0]

清空切片

how-do-you-clear-a-slice-in-go
https://stackoverflow.com/questions/16971741/how-do-you-clear-a-slice-in-go

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func dump(letters []string) {
  6. fmt.Println("letters = ", letters)
  7. fmt.Println(cap(letters))
  8. fmt.Println(len(letters))
  9. for i := range letters {
  10. fmt.Println(i, letters[i])
  11. }
  12. }
  13. func main() {
  14. letters := []string{"a", "b", "c", "d"}
  15. dump(letters)
  16. // clear the slice
  17. letters = nil
  18. dump(letters)
  19. // add stuff back to it
  20. letters = append(letters, "e")
  21. dump(letters)
  22. }

输出结果

  1. letters = [a b c d]
  2. 4
  3. 4
  4. 0 a
  5. 1 b
  6. 2 c
  7. 3 d
  8. letters = []
  9. 0
  10. 0
  11. letters = [e]
  12. 1
  13. 1
  14. 0 e

总结

1.切片是引用类型,数组和切片有着紧密的关联,slice的底层是引用一个数组对象,可以理解为切片是对数组的封装
2一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址。
3切片的长度是变化的,而数组的长度是固定不变的。
4多个不同slice之间可以共享底层的数据

参考

https://www.jianshu.com/p/303daad705a3