Go从入门到放弃5--Slice

210 阅读4分钟

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

数据结构

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。

Go 切片在运行时其实是一个三元组结构,它在 Go 运行时中的表示如下:


type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

我们可以看到,每个切片包含三个字段:array: 是指向底层数组的指针;len: 是切片的长度,即切片中当前元素的个数;cap: 是底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值。

切片与数组的关系非常密切,切片引入了一个抽象层,提供了对数组中部分连续片段的引用,而作为数组的引用,我们可以在运行区间可以修改它的长度和范围。当切片底层的数组长度不足时就会触发扩容,切片指向的数组可能会发生变化,不过在上层看来切片是没有变化的,上层只需要与切片打交道不需要关心数组的变化。多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。

初始化

Go 语言中包含三种初始化切片的方式:

  • 通过下标的方式获得数组或者切片的一部分;
  • 使用字面量初始化新的切片;
  • 使用关键字make创建切片:
arr[0:3] or slice[0:3]
slice := []int{1, 2, 3}
sl := make([]byte, 6, 10) // 其中10为cap值,即底层数组长度,6为切片的初始长度

需要注意的是使用下标初始化切片不会拷贝原数组或者原切片中的数据,它只会创建一个指向原数组的切片结构体,所以修改新切片的数据也会修改原切片。如果没有在 make 中指定 cap 参数,那么底层数组长度 cap 就等于 len。

内置的len和cap函数可以分别获取切片的长度和容量。

slice := make([]int,0)
fmt.Printf("len=%d,cap=%d,slice=%v\n",len(slice),cap(slice)

追加与扩容

内置的append函数用于向slice追加元素:

var s []ints = append(s, 11) 

当我们通过 append 操作向切片追加数据的时候,如果这时切片的 len 值和 cap 值是相等的,也就是说切片底层数组已经没有空闲空间再来存储追加的值了,Go 运行时就会对这个切片做扩容操作,来保证切片始终能存储下追加的新值。扩容时调用了runtime/slice.go文件下的growslice函数

func growslice(et *_type, old slice, cap int) slice {
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  • 如果期望容量大于当前容量的两倍就会使用期望容量;
  • 如果当前切片的长度小于 1024 就会将容量翻倍;
  • 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量; 需要注意的是切片扩容后返回的地址并不一定和原来的地址相同,因此必须小心其可能遇到的陷阱,一般会使用形如a=append(a,T)的方式保证其安全。

在可以预估出元素容量的前提下,使用cap参数创建切片可以提升append的平均操作性能,减少或消除因动态扩容带来的性能损耗

参考资料