加载中...

Golang的slice


概述

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。其实 slice 也就相当于动态数组,长度并不固定,可以用append追加元素,并且slice会在容量不足时自动扩容。

在go语言中文文档中,对于slice有这样的描述:

  1. 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
  2. 切片的长度可以改变,因此,切片是一个可变的数组。
  3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
  4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
  5. 切片的定义:var 变量名 []类型,比如 var str []string var arr []int
  6. 如果 slice == nil,那么 len、cap 结果都等于 0。

slice的数据结构

根据runtime包下的slice.go源码可以看到,slice的基本结构如下:

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
  • array 是指向第一个slice元素对应的底层数组元素的地址的指针;
  • len 是当前切片的长度;
  • cap 是当前切片的容量,即 array 数组的大小:

注意这里len范围内的元素是可以安全访问的,超出这个范围的元素访问会导致panic

一些特性

1、

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠,如下图所示:

如果对共用的底层数组切片进行append添加元素,那么就会开辟新数组,不在共用底层数组,原来的元素拷贝过去,并且在新数组上添加新元素。

2、

和数组不同的是,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就相等。但这样和数组不同的相等测试方法,会让人困惑,如:
func main() {
    // 两个数组相等
    a1 := [3]int{1,2,3}
    a2 := [3]int{1,2,3}
    fmt.Println(a1 == a2)    // true
    // 如果slice使用的是浅相等
    a1 := []int{1,2,3}
    a2 := []int{1,2,3}
    fmt.Println(a1 == a2)   // false,和数组的行为不同,造成困惑
}
  • 如果==使用的是深度相等,和数组的行为保持一致,那也会有下面的问题。正常情况下,将一个slice赋值给另一个slice时,我们只是复制slice的结构体,两个slice的指针都指向同一个底层数组
func main() {
    s1 := s0
    s1[0] = 0   // 通过s1修改,会影响到s0
}

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

扩容规则

在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.cap < 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
          }
       }
    }
.....
}

可以看到,扩容的具体规则为:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

上述过程仅是进行了新容量的预估,接下来还需要根据切片中的元素大小对齐内存

比如新容量是3,int类型,则它需要申请24B的内存,此时它会向语言自身的内存管理模块去申请内存

而内存管理模块会提前向操作系统申请一批内存,分为常用的规格管理起来,我们申请内存时,它会帮我们匹配到足够大,且最接近规格的内存,可能这里内存管理模块分配给你了32B的内存,所以这个时候新容量变成4个了

//runtime下sizeclasses.go文件,基本的内存单元如下
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

参考

  1. 《Go语言设计与实现》
  2. 《Go语言圣经》
  3. 幼麟实验室的Golang合辑
  4. Go语言中文文档

文章作者: DestiNation
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 DestiNation !
  目录