简介

一个 slice 类型一般写作 []T,其中 T 代表 slice 中元素的类型,slice 的语法和数组很像,只是没有固定长度而已。

slice 由三部分构成: 指针、长度和容量

  • 指针: 指向第一个 slice 元素对应的底层数组元素的地址(注:slice 的第一个元素并不一定是数据的第一个元素)
  • 长度: 对应 slice 中元素的数目,长度不能超过容量
  • 容量: 一般是从 slice 的开始位置到底层数据结尾的位置

声明 slice

var identifier []type

创建 slice

var slice1 []type = make([]type, len)

// 也可以简写为
slice1 := make([]type, len)

// 也可以指定容量,其中capacity为可选参数。
slice2 := make([]T, length, capacity)

使用

  • 多个 slice 可以共享底层的数据,并且引用的数组部分区间可能重叠。
  • 要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是 slice 对应结构体本身的指针、长度和容量部分是直接访问的。

创建 slice

内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

在底层,make 创建了一个匿名的数组变量,然后返回一个 slice;只有通过返回的 slice 才能引用底层匿名的数组变量。

  • 在第一种语句中,slice是整个数组的view。
  • 在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

切片操作

slice 的切片操作 s[i:j],其中( 0 ≤ i≤ j≤ cap(s) ),用于创建一个新的 slice,引用 slice 从第 i 个元素到第 j-1 元素的子序列,新的 slice 只会有 j-1 个元素。

以月份为例,因为是月份,所以一月从 1 开始。具体月份省略 https://play.golang.org/p/V-8YDU290jN

package main

import (
	// "fmt"

	"github.com/davecgh/go-spew/spew"
)

func main() {
	months := [...]string{1: "January", 12: "December"}

	spew.Dump(months)

	q2 := months[4:7]
	spew.Dump(q2)

	summer := months[6:9]
	spew.Dump(summer)
}
([13]string) (len=13 cap=13) {
 (string) "",
 (string) (len=7) "January",
 (string) "",
 (string) "",
 (string) "",
 (string) "",
 (string) "",
 (string) "",
 (string) "",
 (string) "",
 (string) "",
 (string) "",
 (string) (len=8) "December"
}
([]string) (len=3 cap=9) {
 (string) "",
 (string) "",
 (string) ""
}
([]string) (len=3 cap=7) {
 (string) "",
 (string) "",
 (string) ""
}

注意 q2 和 summer 的容量,证明了 slice 的容量是指向底层数据结构的结尾位置。具体图解参考下图

如果 切片操作 超出 cap(s) 的上限则会 panic,但是超过 len(s) 则意味着扩展了 slice,因为新的 slice 的长度会变大

示例代码

package main

import (
	"fmt"
)

func main() {
	a := make([]string, 0, 9)

	b := a[:3]
	fmt.Println("success:", b)

	// panic
	c := a[:99]
	fmt.Println(c)
}

slice 的指针

因为 slice 值包含指向第一个 slice 元素的指针,因此想函数传递 slice 将允许修改底层数组的元素。换句话说,复制一个slice 只是对底层的数组创建了一个新的 slice 别名。

不管传的是 slice 还是 slice 指针,如果改变了 slice 底层数组的数据,会反应到实参 slice 的底层数据。

示例代码

package main

import (
	"fmt"
)

func main() {
	var reverse = func(s []int) {
		for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
			s[i], s[j] = s[j], s[i]
		}
	}

	// 简化初始化 slice 的方法
	a := [...]int{0, 1, 2, 3, 4, 5}
	reverse(a[:])
	fmt.Println(a) // "[5 4 3 2 1 0]"
}

slice 之间的比较

slice 唯一合法的比较操作是和 nil 比较

和数组不同的是,slice 之间不能比较,因此我们不能使用 == 操作符来判断两个 slice 是否含有全部相等元素。不过标准库提供了高度优化的 bytes.Equal 函数来判断两个字节型 slice 是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较。

func equal(x, y []string) bool {
	if len(x) != len(y) {
		return false
	}
	for i := range x {
		if x[i] != y[i] {
			return false
		}
	}
	return true
}

为何slice不直接支持比较运算符呢?这方面有两个原因。

  • 一个slice的元素是间接引用的,一个 slice 甚至可以包含自身。虽然有很多办法处理这种情形,但是没有一个是简单有效的。
  • 因为slice的元素是间接引用的,一个固定的slice值(译注:指slice本身的值,不是元素的值)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。而例如Go语言中map的key只做简单的浅拷贝,它要求key在整个生命周期内保持不变性(译注:例如slice扩容,就会导致其本身的值/地址变化)。而用深度相等判断的话,显然在map的key这种场合不合适。对于像指针或chan之类的引用类型,==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的,也能临时解决map类型的key问题,但是slice和数组不同的相等测试行为会让人困惑。因此,安全的做法是直接禁止slice之间的比较操作。(暂时没有很好的理解)

零值的 slice

一个 nil 值的 slice 行为和其他任意 0 长度的 slice 一样。除了文档已经明确说明,所有的 Go 函数应该以相同的方式对待 nil 值的 slice 和 0 长度的 slice

一个零值的 slice 等于 nil。一个 nil 值的 slice 并没有底层数组。一个 nil 值的 slice 的长度和容量都是 0,但是也有非 nil 值的 slice 的长度和容量也是 0,例如 []int{}或者 make([]int,3)[:3]。与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。

var s []int    // len(s) == 0, s == nil
s = nil        // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{}    // len(s) == 0, s != nil

https://play.golang.org/p/yTLtmpvE5Ty

package main

import (
	"fmt"
)

func main() {
	var s []int // len(s) == 0,cap(s) == 0, s == nil

	fmt.Println(len(s), cap(s), s == nil)

	s = nil // len(s) == 0,cap(s) == 0, s == nil

	fmt.Println(len(s), cap(s), s == nil)

	s = []int(nil) // len(s) == 0,cap(s) == 0, s == nil

	fmt.Println(len(s), cap(s), s == nil)

	s = []int{} // len(s) == 0,cap(s) == 0, s != nil

	fmt.Println(len(s), cap(s), s == nil)

	s1 := make([]int, 3)[3:] // len(s) == 0,cap(s) == 0, s != nil
	fmt.Println(len(s1), cap(s1), s1 == nil)
}

如果判断一个 slice 是否为空,应该使用 len(s) == 0 来判断,而不应该使用 s == nil

append

内置的 append 函数用于向 slice 追加元素,并且可以追加多个元素,甚至追加一个slice。

https://play.golang.org/p/M_4fUoK5Paw

package main

import (
	"fmt"
)

func main() {
	var runes []rune
	for _, r := range "Hello, 世界" {
		runes = append(runes, r)
	}
	fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
}

https://play.golang.org/p/iZgN5KlKxBX

package main

import (
	"fmt"
)

func main() {
	var x []int
	x = append(x, 1)
	x = append(x, 2, 3)
	x = append(x, 4, 5, 6)
	x = append(x, x...) // append the slice x
	fmt.Println(x)      // "[1 2 3 4 5 6 1 2 3 4 5 6]"
}

调用 append 函数必须先检测 slice 底层数组是否有足够的容量来保存新添加的元素。

  • 如果有足够空间的话,直接扩展 slice(依然在原有的底层数组之上),将新添加的元素复制到新扩展的空间,并返回slice。
  • 如果没有足够的增长空间的话,append 函数则会先分配一个足够大的 slice 用于保存新的结果,先将旧的 slice 复制到新的空间,然后添加要添加的元素。此时返回的 slice 和 初始的 slice 引用的是不同的底层数组。为了提高内存使用效率,通过在每次扩展数组时直接将长度翻倍(此处有些偏差,当容量小1024时确实是翻倍,当大于 1024 后并不是翻倍,具体看源码分析)从而避免了多次内存分配,也确保了添加单个元素操的平均时间是一个常数时间。

虽然通过循环复制元素更直接,不过内置的 copy 函数可以方便的将 slice 复制到另一个相同类型的 slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice,目标和源的位置顺序和dst = src赋值语句是一致的。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数(我们这里没有用到),等于两个slice中较小的长度,所以我们不用担心覆盖会超出目标slice的范围。

每一次容量的变化都会导致重新分配内存和 copy 操作,注意下面 cap(s) 值大小和 slice 的地址

https://play.golang.org/p/gl8eT_MFBho

package main

import (
	"fmt"
)

func main() {
	var a []int

	for i := 0; i < 10; i++ {
		a = append(a, i)
		fmt.Printf("i: %v,len: %v, cap: %v, addr: %v, a: %v\n", i, len(a), cap(a), &a[0], a)
	}

	// i: 0,len: 1, cap: 2, addr: 0x414020, a: [0]
	// i: 1,len: 2, cap: 2, addr: 0x414020, a: [0 1]
	// i: 2,len: 3, cap: 4, addr: 0x414060, a: [0 1 2]
	// i: 3,len: 4, cap: 4, addr: 0x414060, a: [0 1 2 3]
	// i: 4,len: 5, cap: 8, addr: 0x450020, a: [0 1 2 3 4]
	// i: 5,len: 6, cap: 8, addr: 0x450020, a: [0 1 2 3 4 5]
	// i: 6,len: 7, cap: 8, addr: 0x450020, a: [0 1 2 3 4 5 6]
	// i: 7,len: 8, cap: 8, addr: 0x450020, a: [0 1 2 3 4 5 6 7]
	// i: 8,len: 9, cap: 16, addr: 0x4320c0, a: [0 1 2 3 4 5 6 7 8]
	// i: 9,len: 10, cap: 16, addr: 0x4320c0, a: [0 1 2 3 4 5 6 7 8 9]
}

因此,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量。

源码

slice 的结构定义:

type slice struct {
	array unsafe.Pointer // 指向底层数组的指针
	len   int            // slice 的长度
	cap   int            // slice 的容量
}

结构还是很简单的,slice 主要涉及了 make 和 append 操作,主要看这两个方法的源码

  • make slice

创建是分配一个数组,源码中涉及了内存分配的一些问题(// todo 内存分配)。

func makeslice(et *_type, len, cap int) unsafe.Pointer {
	mem, overflow := math.MulUintptr(et.size, uintptr(cap))
	if overflow || mem > maxAlloc || len < 0 || len > cap {
		mem, overflow := math.MulUintptr(et.size, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}

	return mallocgc(mem, et, true)
}

使用语句 slice := make([]int, 5, 10) 创建 slice 时的示例图

还可以通过数组直接创建 slice,结构如下

func main() {
	array := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
	b := array[5:7]
	fmt.Printf("%#v\n", b)
}

切片从数组 array[5] 开始,到数组 array[7] 结束(不含array[7]),即切片长度为2,数组后面的内容都作为切片的预留内存,即 capacity 为 5。

  • append

当 append 时如果容量充足则直接追加,如果不足则重新申请一个足够大的内存空间,然后把之前的数据拷贝到新的内存空间里,再把数据追加进去。

当向一个capacity为5,且length也为5的Slice再次追加1个元素时,就会发生扩容。

扩容只关心容量,长度会根据追加的数据长度有关。

https://play.golang.org/p/DdDIEncD1BL

package main

import "fmt"

func main() {
	a := make([]int, 5, 5)
	fmt.Println(len(a), cap(a))

	a = append(a, 1)
	fmt.Println(a)
	fmt.Println(len(a), cap(a))

	// output
	// 5 5
	// [0 0 0 0 0 1]
	// 6 10
}

具体扩容的规则如下,当长度小于 1024 时直接双倍,如果大于 1024 则每次扩从 1.25 倍。

if old.len < 1024 {
	newcap = doublecap
} else {
	// Check 0 < newcap to detect overflow
	// and prevent an infinite loop.
	for 0 < newcap && newcap < cap {
		newcap += newcap / 4
	}
	// Set newcap to the requested cap when
	// the newcap calculation overflowed.
	if newcap <= 0 {
		newcap = cap
	}
}

参考链接