Go 的切片

70 阅读4分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。

什么是切片

  • 官方认为:切片是对数组的视图(抽象),一个切片是一个数组片段的描述
  • 数组的大小是固定的,而切片则是为数组元素提供动态大小的、灵活的视图;在实践中,切片比数组更常用
  • 切片的大小是不固定的,可以追加元素,在追加时可能使切片的容量增大,所以切片也可以叫动态数组
  • 切片是一种方便、灵活且强大的包装器
  • 切片与数组相比,不需要设定长度,在[]中不用设定值,相对来说比较自由
  • 切片也可以基于现有的切片或数组生成

语法格式

var identifier []type
  • 切片通过两个下标来界定,即一个上界和一个下界
  • 切片下界的默认值为 0,上界则是该切片的长度。
a[low : high]

和大多数编程语言一样,它也是左闭右开

🌰

a[1:4]

它包含 a 中下标从 1 到 3 的元素

第二个🌰

对于数组var a [10]int来说,以下切片是等价的

a[0:10]
a[:10]
a[0:]
a[:]

简单的🌰

package main

import "fmt"

func main() {
    // 数组
	primes := [6]int{2, 3, 5, 7, 11, 13}
	
    // 切片
	var s []int = primes[1:4]
	fmt.Println(s)
}

运行结果

[3 5 7]

切片就像数组的引用

  • 切片并不存储任何数据,它只是描述了底层数组中的一段元素
  • 更改切片的元素会修改其底层数组中对应的元素
  • 与它共享底层数组的切片都会同步这些修改
package main

import "fmt"

func main() {
	arrs := [5]int{1, 2, 3, 4, 5}
	fmt.Println(arrs)

	// 取第一、第二个元素
	a := arrs[0:2]
	// 取第二、第三个元素
	b := arrs[1:3]
	fmt.Println(a, b)

	// 修改切片 b 的第一个元素
	b[0] = 100
	fmt.Println(a, b)
	fmt.Println(arrs)
}

运行结果

[1 2 3 4 5]
[1 2] [2 3]
[1 100] [100 3]
[1 100 3 4 5]

可以看到底层数组 names 和切片 a 都同步修改了对应的值

原理图

  • a、b 两个切片只是 arrs 数组的视图,切片下标指向的是底层数组对应的值
  • 所以修改 b[0] 等同于修改 arrs[1],也会同步修改 a[1],因为它也指向 arrs[1]

Reslice

  • 就是对切片进行切片
  • 切片保存了对底层数组的引用,若将某个切片赋予另一个切片,它们会指向(引用)同一个数组
package main

import "fmt"

func main() {
    // 切片 s 的底层数组是:[1, 2, 3, 4, 5, 6]
	s := []int{1, 2, 3, 4, 5, 6}

	s = s[1:4]
	fmt.Println(s)

	s = s[:2]
	fmt.Println(s)

	s = s[1:]
	fmt.Println(s)
}

运行结果

[2 3 4]
[2 3]
[3]

原理图

s 不断对自己进行切片,但他们都是底层数组[1, 2, 3, 4, 5, 6]的视图

slice 的实现

slice 像一个结构体

  • ptr:指向数组第一个元素的指针
  • len:切片长度,切片中的元素数量,通过切片下标取值只能取 len 以内的值[0, len(s) -1]
  • cap:切片容量,底层数组的元素数量(从切片指针 ptr 开始)

slice 的扩展

package main

import "fmt"

func main() {
	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
    
	s1 := arr[2:6]
	s2 := s1[3:5]
    
	fmt.Println(s1, s2)
}

运行结果

[2 3 4 5] [5 6]

疑问?

为什么 s1 只有四个元素而且没有 6,但是 s2 取第五个元素的时候没有报错,而且返回的是 6?

难道可以直接 s1[4] 来取值?

如果直接打印 s1[4] 会报错

panic: runtime error: index out of range [4] with length 4

goroutine 1 [running]:
main.main()
	/tmp/sandbox1066338689/prog.go:10 +0x71

原理图

  • 上面也讲了 slice 的实现原理
  • 核心:slice 可以向后扩展,只要不超过底层数组/切片的 cap 就可以继续取值
  • s1 := arr[2:6]:取的值是 2-5,对应的下标是 0-3,但底层数组后面仍然有值,若有需要 s1 可以扩展取值第 4、5 位元素;所以 s1 的 len 是 4,cap 是 6
  • s2 := s1[3:5]:s1 并没有第 5 位元素,所以 s2 要进行扩展,就会取到底层数组对应位置的值,就是 6;所以 s2 的 len 是 2,cap 是 3

看看 s1、s2 的 len、cap

package main

import "fmt"

func main() {
	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
	s1 := arr[2:6]
	s2 := s1[3:5]

	fmt.Printf("s1=:%v, len: %v, cap: %v \n", s1, len(s1), cap(s1))
	fmt.Printf("s2=:%v, len: %v, cap: %v", s2, len(s2), cap(s2))
}

运行结果

s1=:[2 3 4 5], len: 4, cap: 6 
s2=:[5 6], len: 2, cap: 3

总结

  • slice 可以向后扩展,不可以向前扩展
  • s[i] 最大不可以超越 len(s)
  • 向后扩展不可以超越底层数组/切片的 cap(s)
package main

import "fmt"

func main() {
	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
	s1 := arr[2:6]
	s2 := s1[3:5]
	
    // s1 的 cap 是 6,所以上边界最大取 6
	s3 := s1[3:6]
    
    // s2 的 cap 是 3,所以上边界最大取 3
	s4 := s2[1:3]
}

空切片

  • 切片的零值是 nil
  • nil 切片的长度和容量为 0 且没有底层数组
package main

import "fmt"

func main() {
	var s []int
	fmt.Println(s, len(s), cap(s))
	if s == nil {
		fmt.Println("nil!")
	}
}

运行结果

[] 0 0
nil!