slice

本文最后更新于:2023年12月5日 晚上

slice 是数组的引用,但是本身是结构体:

// runtime/slice.go
type slice struct {
 array unsafe.Pointer  // 指向slice中第一个元素的指针
 len   int     // slice的长度,长度是下标操作的上界,如x[i]中i必须小于长度
 cap   int     // slice的容量,容量是分割操作的上界,如x[i:j]中j不能大于容量。
}

len 和 cap

s := make([]string, 1, 3)
fmt.Println(s[0])           // ""
fmt.Println(len(s), cap(s)) // 1 3
s = append(s, "c", "d", "e", "f")
fmt.Println(len(s), cap(s)) // 4 6

创建 slice:

  1. 创建一个长度为cap的数组,如果不指定cap,则cap等于len;例如s := []string{"a","b","c"}lencap都是 3;
  2. 将数组前len个元素进行初始化,上例中数组第一个元素 k 初始化为空字符串;
  3. 返回。

slice 的分割

slice 的分割不涉及复制操作:它只是新建了一个结构来放置一个不同的指针,长度和容量:

分割表达式x[1:3]并不分配更多的数据:它只是创建了一个新的 slice 来引用相同的存储数据。

s1 := []int{1, 2, 3}
s2 := s1[0:] // 等价于 s2 = s1
s1[0] = 100
fmt.Println(s2)    // [100 2 3]

修改 s1,也会影响到 s2。

字符串的分割也同理:

append 和 copy

append

现有的元素加上要添加的元素,长度不超过 cap,则不会发生扩容行为,只会修改被引用的数组和len

s1 := make([]int, 2, 100)
s2 := s1
s1 = append(s1, 1)
fmt.Printf("%p len:%d cap:%d\n", s1, len(s1), cap(s1)) // 0xc00010a000 len:3 cap:100
fmt.Printf("%p len:%d cap:%d\n", s2, len(s2), cap(s2)) // 0xc00010a000 len:2 cap:100
fmt.Println(s1)  // [0 0 1]
fmt.Println(s2)  // [0 0]

append 添加的元素太多,当前底层的数组不够用了,就会自动扩容,会复制被引用的数组,然后切断引用关系。

copy

上面的例子中:

s1 := []int{1, 2, 3}
s2 := s1[0:] // 等价于 s2 = s1
s1[0] = 100
fmt.Println(s2)    // output:[100 2 3]

修改 s1,也会影响到 s2,如果想避免这种情况,需要使用copy(dst, src)

s1 := []int{1, 2, 3}
s2 := make([]int, len(s1))
copy(s2, s1)
s1[0] = 100
fmt.Println(s2)  // output:[1 2 3]

slice 的扩容

在对 slice 进行 append 等操作时,可能导致 slice 会自动扩容,重新分配更大的数组。go1.18 之前其扩容的策略是:

  1. 如果新的大小是当前大小 2 倍以上,则大小增长为新大小;
  2. 否则循环操作:如果当前大小小于 1024,按每次 2 倍增长,否则每次按当前大小 1/4 增长。

go1.18 之后,优化了切片扩容的策略 2,让底层数组大小的增长更加平滑:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
    newcap = cap
} else {
    const threshold = 256
    if old.cap < threshold {
        newcap = doublecap
    } else {
        // Check 0 < newcap to detect overflow
        // and prevent an infinite loop.
        for 0 < newcap && newcap < cap {
            // Transition from growing 2x for small slices
            // to growing 1.25x for large slices. This formula
            // gives a smooth-ish transition between the two.
            newcap += (newcap + 3*threshold) / 4
        }
        // Set newcap to the requested cap when
        // the newcap calculation overflowed.
        if newcap <= 0 {
            newcap = cap
        }
    }
}

通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从 2 到 1.25 的突变,作者给出了几种原始容量下对应的“扩容系数”:

原始容量 扩容系数
256 2.0
512 1.63
1024 1.44
2048 1.35
4096 1.30

什么时候用 slice?

在 go 语言中 slice 是很灵活的,大部分情况都能表现的很好,但也有特殊情况。

当程序要求 slice 的容量超大并且需要频繁的更改 slice 的内容时,就不应该用 slice,改用list更合适。


slice
http://blog.lujinkai.cn/Golang/slice/
作者
像方便面一样的男子
发布于
2023年2月23日
更新于
2023年12月5日
许可协议