golang 切片详解

1. 切片的定义

切片的结构定义在 reflect.SliceHeader

type SliceHeader struct{
  Data uintptr
  Len int
  Cap int      
}

 

看看切片的几种定义方式:

var (
    a []int               // nil切片,和nil相等,一般用来表示一个不存在的切片
    b = []int{}           // 空切片,和nil不相等,一般用来表示一个空的集合
    c = []int{1, 2, 3}    // 有3个元素的切片,len=3,cap=3
    d = c[:2]             // 有2个元素的切片,len=2,cap=3
    e = c[0:2:cap(c)]     // 有2个元素的切片,len=2,cap=3
    f = c[:0]             // 有0个元素的切片,len=0,cap=3
    g = make([]int, 3)    // 有3个元素的切片,len=3,cap=3
    h = make([]int, 2, 3) // 有2个元素的切片,len=2,cap=3
    i = make([]int, 0, 3) // 有0个元素的切片,len=0,cap=3
)

 

2. 切片的内存

下面的TrimSpace()函数用于删除[]byte中的空格。函数的实现利用了长度为0的切片的特性,实现简单而高效:

func TrimSpace(s []byte) []byte {
    b := s[:0]
    for _, x := range s {
        if x != ' ' {
            b = append(b, x)
        }
    }
    return b
}

 

其实类似的根据过滤条件原地修改切片元素的算法都可以采用类似的处理方式,因为是删除操作,所以不会出现内存不足的情况。

func Filter(s []byte, fn func(x byte) bool) []byte {
    b := s[:0]
    for _, x := range s {
        if !fn(x) {
            b = append(b, x)
        }
    }
    return b
}

 

切片高效操作的要点是要降低内存分配的次数,尽量保证append()操作不会超出cap,降低触发内存分配次数和每次分配内存的大小。

切片操作不会复制底层的数组,底层数组会被保存在内存中,直到它不再被引用。但是有时候可能会因为一个小的内存引用而导致整个底层数组属于被使用的状态,这时会延迟垃圾回收对底层数组的回收。

例如下面这一段代码,FindPhoneNumber()函数加载整个文件到内存中,然后搜索第一个出现的电话号码,最后结果以切片方式返回:

func FindPhoneNUmber(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return regexp.MustCompile("[0-9]+").Find(b)
}

 

这段代码返回的[]byte指向保存整个文件的数组。由于切片引用了整个原始数组,导致垃圾回收不能及时释放底层数组的空间,一个小小的需求就可能系统需要长时间保留整个文件数据。

要解决这个问题,通常需要将需要的数据复制到一个新的切片中,虽然值传递有一定的代价,但是切断了对原始数组的依赖

func FindPhoneNUmber(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = regexp.MustCompile("[0-9]+").Find(b)
    return append([]byte{}, b...)
}

 

类似的问题可能在删除切片元素时会遇到。假设切片里存放的是指针对象,那么删除末尾元素后,被删除的元素依然会被切片底层数组引用,导致不能够被及时回收。

保险的方式是先讲指向需要提前回收内存的指针设置为nil,保证垃圾回收器可以发现需要回收的对象,再进行切片的删除:

var a []*int{...}
a[len(a)-1] = nil
a = a[:len(a)-1]

 

3. slice append的扩容

首先 Append 判断类型是否 slice,然后调用 grow 扩容,从 l1 <= m 的判断可以发现确实容量足够的情况下,只是对原始数组建立一个新的 slice

但当容量不足时,可以看到只有在当前元素 i0 小于1024时,才是按2倍速度正常,否则其实每次只增长25%。

其次,在扩容时,应该是按照2^n做一次内存向上取整的。

posted @ 2022-03-18 20:06  aganippe  阅读(1656)  评论(0编辑  收藏  举报