广告
返回顶部
首页 > 资讯 > 精选 >Go语言切片是怎么扩容的
  • 688
分享到

Go语言切片是怎么扩容的

2023-07-06 00:07:03 688人浏览 泡泡鱼
摘要

本篇内容主要讲解“Go语言切片是怎么扩容的”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Go语言切片是怎么扩容的”吧!在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。切片是一

本篇内容主要讲解“Go语言切片是怎么扩容的”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Go语言切片是怎么扩容的”吧!

    在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。

    切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。

    切片是一种引用类型,它有三个属性:指针长度容量

    Go语言切片是怎么扩容的

    底层源码定义如下:

    type slice struct {    array unsafe.Pointer    len   int    cap   int}
    • 指针: 指向 slice 可以访问到的第一个元素。

    • 长度: slice 中元素个数。

    • 容量: slice 起始元素到底层数组最后一个元素间的元素个数。

    比如使用 make([]byte, 5) 创建一个切片,它看起来是这样的:

    Go语言切片是怎么扩容的

    声明和初始化

    切片的使用还是比较简单的,这里举一个例子,直接看代码吧。

    func main() {    var nums []int  // 声明切片    fmt.Println(len(nums), cap(nums)) // 0 0    nums = append(nums, 1)   // 初始化    fmt.Println(len(nums), cap(nums)) // 1 1    nums1 := []int{1,2,3,4}    // 声明并初始化    fmt.Println(len(nums1), cap(nums1))    // 4 4    nums2 := make([]int,3,5)   // 使用make()函数构造切片    fmt.Println(len(nums2), cap(nums2))    // 3 5}

    扩容时机

    当切片的长度超过其容量时,切片会自动扩容。这通常发生在使用 append 函数向切片中添加元素时。

    扩容时,Go 运行时会分配一个新的底层数组,并将原始切片中的元素复制到新数组中。然后,原始切片将指向新数组,并更新其长度和容量。

    需要注意的是,由于扩容会分配新数组并复制元素,因此可能会影响性能。如果你知道要添加多少元素,可以使用 make 函数预先分配足够大的切片来避免频繁扩容。

    接下来看看 append 函数,签名如下:

    func Append(slice []int, items ...int) []int

    append 函数参数长度可变,可以追加多个值,还可以直接追加一个切片。使用起来比较简单,分别看两个例子:

    追加多个值:

    package mainimport "fmt"func main() {    s := []int{1, 2, 3}    fmt.Println("初始切片:", s)    s = append(s, 4, 5, 6)    fmt.Println("追加多个值后的切片:", s)}

    输出结果为:

    初始切片: [1 2 3]
    追加多个值后的切片: [1 2 3 4 5 6]

    再来看一下直接追加一个切片:

    package mainimport "fmt"func main() {    s1 := []int{1, 2, 3}    fmt.Println("初始切片:", s1)    s2 := []int{4, 5, 6}    s1 = append(s1, s2...)    fmt.Println("追加另一个切片后的切片:", s1)}

    输出结果为:

    初始切片: [1 2 3]
    追加另一个切片后的切片: [1 2 3 4 5 6]

    再来看一个发生扩容的例子:

    package mainimport "fmt"func main() {    s := make([]int, 0, 3) // 创建一个长度为0,容量为3的切片    fmt.Printf("初始状态: len=%d cap=%d %v\n", len(s), cap(s), s)    for i := 1; i <= 5; i++ {        s = append(s, i) // 向切片中添加元素        fmt.Printf("添加元素%d: len=%d cap=%d %v\n", i, len(s), cap(s), s)    }}

    输出结果为:

    初始状态: len=0 cap=3 []
    添加元素1: len=1 cap=3 [1]
    添加元素2: len=2 cap=3 [1 2]
    添加元素3: len=3 cap=3 [1 2 3]
    添加元素4: len=4 cap=6 [1 2 3 4]
    添加元素5: len=5 cap=6 [1 2 3 4 5]

    在这个例子中,我们创建了一个长度为 0,容量为 3 的切片。然后,我们使用 append 函数向切片中添加 5 个元素。

    当我们添加第 4 个元素时,切片的长度超过了其容量。此时,切片会自动扩容。新的容量是原始容量的两倍,即 6

    表面现象已经看到了,接下来,我们就深入到源码层面,看看切片的扩容机制到底是什么样的。

    源码分析

    在 Go 语言的源码中,切片扩容通常是在进行切片的 append 操作时触发的。在进行 append 操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用 growslice 函数进行扩容。

    growslice 函数定义在 Go 语言的 runtime 包中,它的调用是在编译后的代码中实现的。具体来说,当执行 append 操作时,编译器会将其转换为类似下面的代码:

    slice = append(slice, elem)

    在上述代码中,如果切片容量不足以容纳新的元素,则会调用 growslice 函数进行扩容。所以 growslice 函数的调用是由编译器在生成的机器码中实现的,而不是在源代码中显式调用的

    切片扩容策略有两个阶段,go1.18 之前和之后是不同的,这一点在 go1.18 的 release notes 中有说明。

    下面我用 go1.17 和 go1.18 两个版本来分开说明。先通过一段测试代码,直观感受一下两个版本在扩容上的区别。

    package mainimport "fmt"func main() {    s := make([]int, 0)    oldCap := cap(s)    for i := 0; i < 2048; i++ {        s = append(s, i)        newCap := cap(s)        if newCap != oldCap {            fmt.Printf("[%d -> %4d] cap = %-4d  |  after append %-4d  cap = %-4d\n", 0, i-1, oldCap, i, newCap)            oldCap = newCap        }    }}

    上述代码先创建了一个空的 slice,然后在一个循环里不断往里面 append 新元素。

    然后记录容量的变化,每当容量发生变化的时候,记录下老的容量,添加的元素,以及添加完元素之后的容量。

    这样就可以观察,新老 slice 的容量变化情况,从而找出规律。

    运行结果(1.17 版本):

    [0 ->   -1] cap = 0     |  after append 0     cap = 1   
    [0 ->    0] cap = 1     |  after append 1     cap = 2   
    [0 ->    1] cap = 2     |  after append 2     cap = 4   
    [0 ->    3] cap = 4     |  after append 4     cap = 8   
    [0 ->    7] cap = 8     |  after append 8     cap = 16  
    [0 ->   15] cap = 16    |  after append 16    cap = 32  
    [0 ->   31] cap = 32    |  after append 32    cap = 64  
    [0 ->   63] cap = 64    |  after append 64    cap = 128 
    [0 ->  127] cap = 128   |  after append 128   cap = 256 
    [0 ->  255] cap = 256   |  after append 256   cap = 512 
    [0 ->  511] cap = 512   |  after append 512   cap = 1024
    [0 -> 1023] cap = 1024  |  after append 1024  cap = 1280
    [0 -> 1279] cap = 1280  |  after append 1280  cap = 1696
    [0 -> 1695] cap = 1696  |  after append 1696  cap = 2304

    运行结果(1.18 版本):

    [0 ->   -1] cap = 0     |  after append 0     cap = 1
    [0 ->    0] cap = 1     |  after append 1     cap = 2   
    [0 ->    1] cap = 2     |  after append 2     cap = 4   
    [0 ->    3] cap = 4     |  after append 4     cap = 8   
    [0 ->    7] cap = 8     |  after append 8     cap = 16  
    [0 ->   15] cap = 16    |  after append 16    cap = 32  
    [0 ->   31] cap = 32    |  after append 32    cap = 64  
    [0 ->   63] cap = 64    |  after append 64    cap = 128 
    [0 ->  127] cap = 128   |  after append 128   cap = 256 
    [0 ->  255] cap = 256   |  after append 256   cap = 512 
    [0 ->  511] cap = 512   |  after append 512   cap = 848 
    [0 ->  847] cap = 848   |  after append 848   cap = 1280
    [0 -> 1279] cap = 1280  |  after append 1280  cap = 1792
    [0 -> 1791] cap = 1792  |  after append 1792  cap = 2560

    根据上面的结果还是能看到区别的,具体扩容策略下面边看源码边说明。

    go1.17

    扩容调用的是 growslice 函数,我复制了其中计算新容量部分的代码。

    // src/runtime/slice.gofunc 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            }        }    }    // ...    return slice{p, old.len, newcap}}

    在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

    • 如果期望容量大于当前容量的两倍就会使用期望容量;

    • 如果当前切片的长度小于 1024 就会将容量翻倍;

    • 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

    go1.18

    // src/runtime/slice.gofunc growslice(et *_type, old slice, cap int) slice {    // ...    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            }        }    }    // ...    return slice{p, old.len, newcap}}

    和之前版本的区别,主要在扩容阈值,以及这行代码:newcap += (newcap + 3*threshold) / 4

    在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

    • 如果期望容量大于当前容量的两倍就会使用期望容量;

    • 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;

    • 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是 newcap + 3*threshold,直到新容量大于期望容量;

    内存对齐

    分析完两个版本的扩容策略之后,再看前面的那段测试代码,就会发现扩容之后的容量并不是严格按照这个策略的。

    那是为什么呢?

    实际上,growslice 的后半部分还有更进一步的优化(内存对齐等),靠的是 roundupsize 函数,在计算完 newcap 值之后,还会有一个步骤计算最终的容量:

    capmem = roundupsize(uintptr(newcap) * ptrSize)newcap = int(capmem / ptrSize)

    到此,相信大家对“Go语言切片是怎么扩容的”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

    --结束END--

    本文标题: Go语言切片是怎么扩容的

    本文链接: https://www.lsjlt.com/news/356297.html(转载时请注明来源链接)

    有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

    本篇文章演示代码以及资料文档资料下载

    下载Word文档到电脑,方便收藏和打印~

    下载Word文档
    猜你喜欢
    • Go语言切片是怎么扩容的
      本篇内容主要讲解“Go语言切片是怎么扩容的”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Go语言切片是怎么扩容的”吧!在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。切片是一...
      99+
      2023-07-06
    • 一文详解Go语言切片是如何扩容的
      目录声明和初始化扩容时机源码分析go1.17go1.18内存对齐总结在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。 切片是一个拥有相同类型元素的可变长度的序列,...
      99+
      2023-05-15
      Go语言切片扩容 Go语言切片 Go 切片扩容 Go 切片
    • go语言切片是什么
      go语言切片是对数组的一种抽象。切片是对数组的引用,包含了指向数组元素的指针、切片的长度和容量。创建切片的方式有:1、使用内置的make函数创建一个切片;2、使用切片的字面量创建一个切片;3、从另一个切片创建新的切片。可以使用内置的appe...
      99+
      2023-12-11
      Golang go语言
    • go的切片扩容机制是什么
      本篇内容主要讲解“go的切片扩容机制是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“go的切片扩容机制是什么”吧!切片的扩容策略?如何扩容?扩容策略:如果切片的容量小于 1024 个元素,于...
      99+
      2023-07-05
    • go语言切片怎么生成
      本篇内容介绍了“go语言切片怎么生成”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!在go语言中,切片(slice)是对数组的一个连续片段的引...
      99+
      2023-07-04
    • go的切片扩容机制详解
      切片的扩容策略?如何扩容? 扩容策略:如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。总容量从原来的1个翻倍到现在的2个。 一旦元素个数超过 1024 个元素,那...
      99+
      2023-05-14
      go 切片扩容
    • Go语言中的数组和切片是什么
      本篇内容介绍了“Go语言中的数组和切片是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!目录 数组 切片(Slice)append 函数1...
      99+
      2023-06-20
    • go语言中切片的长度和容量的区别
      切片的长度,显而易见的就是元素的个数,根据元素的个数进行返回具体的长度。 切片的长度,更像是一个警戒值,如果长度与容量相等,就会进行容量的扩容,比如 des :=make([...
      99+
      2022-06-07
      GO 长度 go语言
    • Go语言里切片slice怎么初始化
      这篇文章主要介绍“Go语言里切片slice怎么初始化”,在日常操作中,相信很多人在Go语言里切片slice怎么初始化问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Go语言里切片slice怎么初始化”的疑惑有所...
      99+
      2023-07-02
    • Go语言容器的数组和切片如何使用
      这篇文章主要介绍“Go语言容器的数组和切片如何使用”,在日常操作中,相信很多人在Go语言容器的数组和切片如何使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Go语言容器的数组和切片如何使用”的疑惑有所帮助!...
      99+
      2023-07-04
    • go语言中切片和数组指的是什么
      本文小编为大家详细介绍“go语言中切片和数组指的是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“go语言中切片和数组指的是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。在go语言中,数组是一个由固定长度...
      99+
      2023-07-04
    • 聊聊go语言中的切片(slice)
      本篇文章带大家了解一下golang中可变长度的"数组"——切片(slice),希望对大家有所帮助!golang切片(slice)(1)切片的定义切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做...
      99+
      2022-07-04
      go语言 golang 切片
    • go语言二维数组切片怎么定义
      在Go语言中,可以使用make函数来创建二维切片。具体的定义方式如下: slice := make([][]int, numRows...
      99+
      2023-10-21
      go语言
    • Go语言怎么实现切片增删改查
      本篇内容主要讲解“Go语言怎么实现切片增删改查”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Go语言怎么实现切片增删改查”吧!引言Golang 的数组是固定长度,可以容纳相同数据类型的元素的集合...
      99+
      2023-06-30
    • Go 语言中怎么实现数组与切片
      本篇文章为大家展示了Go 语言中怎么实现数组与切片,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。数组数组是一组类型相同的,长度固定的,按数字编号排列的数据序列。由于 go  语言中,数组的...
      99+
      2023-06-15
    • 浅析Go语言容器之数组和切片的使用
      目录序列容器数组VectorDequeList单链表总结在 Java 的核心库中,集合框架可谓鼎鼎大名:Array 、List、Set、Queue、HashMap 等等,随便拎一个出...
      99+
      2022-11-11
    • golang切片扩容的方法是什么
      在Go语言中,切片的扩容是通过使用内置函数append()来实现的。当切片的长度不够容纳新元素时,append()函数会创建一个新的...
      99+
      2023-10-21
      golang
    • 详解Go语言中切片的长度与容量的区别
      目录切片的声明切片的长度和容量切片追加元素后长度和容量的变化append 函数切片的源代码学习切片的结构体切片的扩容总结切片的声明 切片可以看成是数组的引用(实际上切片的底层数据结构...
      99+
      2022-11-11
    • 怎么使用Go语言的copy()实现切片复制
      本文小编为大家详细介绍“怎么使用Go语言的copy()实现切片复制”,内容详细,步骤清晰,细节处理妥当,希望这篇“怎么使用Go语言的copy()实现切片复制”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。Go语言的...
      99+
      2023-07-05
    • go语言中的二维切片赋值
      先用append填充一维的,然后将一维append到二维 代码如下 var a [][]int for i := 0; i < 10; i++ { var t...
      99+
      2022-06-07
      GO 二维 赋值 go语言
    软考高级职称资格查询
    编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
    • 官方手机版

    • 微信公众号

    • 商务合作