Golang - 深入解析slice 扩容底层实现

1、切片的定义

切片就是一种简化版的动态数组,长度不固定,便于使用和管理数据集合。

切片是对数组一个连续片段的引用,所以切片是一个引用类型。

2、切片的内部结构

type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
}

内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定区域内。 

1)指针,指向底层数组中切片指定的开始位置,一块连续的内存;

2)长度,即元素的数量;

3)容量,即数组的最大长度,也就是切片开始位置到数组的最后位置的长度。

3、切片的基础操作

1)、 定义

var s []int               // var identifier []type
var numbers = make([]int, 3, 5)    //var numbers = make([]T, length, capacity)

2)、初始化

A:直接初始化

s := []int {1,2,3 }
s :=make([]int,len,cap)

B:通过数组截取初始化  arr := [5]int {1,2,3,4,5}

a)s := arr[:]
切⽚中包含数组的所有元素;

b)s := arr[startIndex:endIndex]
将arr中从下标startIndex到endIndex-1 下的元素创建为⼀个新的切⽚(前闭后开),⻓度为endIndex-startIndex;

c)s := arr[startIndex:]
索引在冒号左边,表示截取索引startInex左边的所有元素,即元素范围:arr[0]~arr[strartIndex-1],一共有startIndex个元素;

d)s := arr[:endIndex]
索引在冒号右边,表示截取索引endInex右边的所有元素,即元素范围:arr[endIndex]~arr[len(arr) - 1], 一共有len(arr) - endIndex个元素

3)、删除

要删除元素的下标index,比较取巧的方法是:

要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

var s1 = []int{1, 2, 3, 4}                 // 初始化一个切片
var index = 2                              // 要删除的下标
s1 = append(s1[:index], s1[index+1:]...)   // 删除下标为index的元素

说明:s1[:index]指的是[0, index)区间的元素,注意是左闭右开,s1[index+1:]指的是[index+1, len)区间的元素,使用append将这两个区间合并后,刚好把index下标的元素干掉。

注意分隔操作符" :"的使用,有以下几个特点:

i) 分隔符:可以对数组或者slice做数据截取,:得到的结果是一个新slice。

ii) 新slice结构体里的array指针指向原数组或者原slice的底层数组,新切片的长度是:右边的数值减去左边的数值,新切片的容量是原切片的容量减去:左边的数值。

iii) 分隔符:的左边如果没有写数字,默认是0,右边没有写数字,默认是被分割的数组或被分割的切片的长度。

a := make([]int, 0, 4)     // a的长度是0,容量是4
b := a[:]                  // 等价于 b := a[0:0], b的长度是0,容量是4
c := a[:1]                 // 等价于 c := a[0:1], b的长度是1,容量是4
d := a[1:]                 // 编译报错 panic: runtime error: slice bounds out of range
e := a[1:4]                // [0 0 0],e的长度3,容量3

iiii) 分割操作符: 右边的数值有上限,上限有2种情况

如果分割的是数组,那上限是是被分割的数组的长度。

如果分割的是切片,那上限是被分割的切片的容量。注意,这个和下标操作不一样,如果使用下标索引访问切片,下标索引的最大值是(切片的长度-1),而不是切片的容量。

iiiii) 对数组或者切片做:分割操作产生的新切片还是指向原来的底层数组,并不会把原底层数组的元素拷贝一份到新的内存空间里。

4)、空切片

一个切片在未初始化之前默认为 nil,长度为 0 

4、切片的扩容

1)、追加

var s1 = []int{1, 2}     // 初始化一个切片
s1 = append(s1, 3)       // 在最后添加一个元素

遵循两个原则:

A: append函数调用后,应该使用返回值作为结果。
B: append函数调用后,不应该再使用实参传入的slice。

使用append函数一般都是s = append(s,elem1)这种用法,也就是把结果重新赋值给原来的slice。

2)、扩容机制

结论:

  • 当需要的容量大于原切片容量的两倍时,会使用需要的容量作为新容量。
  • 当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。

3)、示例(查看这里

4)、解读源码

// 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
            }
        }
    }
// ...省略部分
}

4)、扩容后数组的指向

A: 若原切片还有容量可以扩容,执行append操作是对原有切片进行,所以这种情况下,扩容以后的数组还是指向原来的数组,扩容后的数组中某一个值的修改会影响原切片与原数组

B: 向原有切片新增一个元素生成新切片时,若原切片已满,Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作,这种情况不影响原数组

5、切片的复制(copy)

将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组或切片的元素个数进行复制。

copy( dstSlice, srcSlice []T) int

copy会从原切片srcSlice拷贝 min(len(dstSlice), len(srcSlice))个元素到目标切片dstSlice

因为拷贝的元素个数min(len(dst), len(src))不会超过目标切片的长度len(dst),所以copy执行后,目标切片的长度不会变,容量不会变,只改变元素内容。

slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) //slice2: [1 2 3]      只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) //slice1: [5 4 3 4 5]  只会复制slice2的3个元素到slice1的前3个位置

6、总结

Golang中的切片扩容机制,与切片的数据类型、原本切片的容量、所需要的容量都有关系,比较复杂。对于常见数据类型,在元素数量较少时,大致可以认为扩容是按照翻倍进行的,但具体情况需要具体分析。

为了避免因为切片是否发生扩容的问题导致bug,最好的处理办法还是在必要时使用 copy 来复制数据,保证得到一个新的切片,以避免后续操作带来预料之外的副作用。

posted @   李若盛开  阅读(445)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示