go

go切片扩容机制理解

Posted by hebicheng on May 12, 2021

go 切片(slice)

go切片是go中十分方便的一种包装,它本身不拥有任何数据,只是对现有的数组进行引用。但是在切片的容量(capaciy)发生改变时,切片指向的数组地址时候改变的,也就是说,在切片容量发生改变时,go内部会申请一个新的数组,然后将原数组中的数据复制到新数组中,同时切片指向新的数组。

下面我们通过代码来实际感受一下。

1
2
3
4
5
s := [...]int {1,2,3}
sSlice := s[:1]
fmt.Println("s:", s, ", sSlice", sSlice)
sSlice[0] = 2
fmt.Println("s:", s, ", sSlice", sSlice)

输出:

1
2
s: [1 2 3] , sSlice: [1]
s: [2 2 3] , sSlice: [2]

在这段代码中sSlice是对数组s的一个切片,当我们将sSlice的第一个元素修改后,数组s的值也相应修改。

1
2
3
4
5
s := [...]int {1,2,3}
sSlice := s[:1]
fmt.Println("s:", s, ", sSlice:", sSlice)
sSlice = append(sSlice, 5)
fmt.Println("s:", s, ", sSlice:", sSlice)

输出:

1
2
s: [1 2 3] , sSlice: [1]
s: [1 5 3] , sSlice: [1 5]

同理,当以append的方式往切片中加入数据时,也会导致原数组的改变,注意此时切片的容量是没有改变的:依然为数组s的长度3

下面我们来编写一种会导致切片容量改变的情况。

1
2
3
4
5
s := [...]int {1,2,3}
sSlice := s[:1]
fmt.Println("s:", s, ", sSlice:", sSlice)
sSlice = append(sSlice, 5, 6, 7)
fmt.Println("s:", s, ", sSlice:", sSlice)

输出:

1
2
s: [1 2 3] , sSlice: [1]
s: [1 2 3] , sSlice: [1 5 6 7]

现在由于我们往切片中添加了3个元素,原来切片的容量3已经无法存储下以前切片中的1一个元素和添加的3个元素,共4个元素了,所以切片会自动扩容。而当切片扩容后,此时的append函数返回的就是一个新的切片对新的数组的引用了,而添加的值也是添加到新的切片中(实际是底层的新数组)。

切片的扩容

上面我们知道了切片在发生扩容时会只想一个新的数组,所以我们在编码的时候就需要知道切片什么时候会发生扩容,以便于我们规避由切片指向的数组改变而我们没有意识到而产生bug。

我们先看看go切片扩容的相关源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/runtime/slice.go
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 {
            // 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
            }
        }
    }
// ...省略部分
}
  • cap大于切片原容量的两倍时,切片的容量会增加为cap的值
    1
    2
    3
    4
    
    cap = old.cap + append传入元素个数
    if cap != 1 and cap%2 != 0 {
      cap += 1 // 当 cap!=1 时,保证cap是偶数
    }
    
  • 如果cap在切片原容量的两倍范围内时,需要区分原数组长度是否小于1024,如果小于,直接容量加倍,如果大于,容量以1/4为增量增加,直至容量符合需要的大小。

原创作品,转载请注明来源 https://hebicheng.github.io