golang中slice扩容的具体实现

 更新时间:2025年05月23日 10:42:30   作者:xsh219  
Go 语言中的切片扩容机制是 Go 运行时的一个关键部分,它确保切片在动态增加元素时能够高效地管理内存,本文主要介绍了golang中slice扩容的具体实现,感兴趣的可以了解一下

Go 语言中的切片扩容机制是 Go 运行时的一个关键部分,它确保切片在动态增加元素时能够高效地管理内存。这个机制是在 Go 运行时内部实现的,涉及了内存分配、数据拷贝和容量调整。扩容的实现主要体现在 runtime.growslice 函数中。下面我们将深入分析 Go 切片的扩容机制,结合源码来进行详细解答。

1. 切片扩容的触发

切片是 Go 中的一种动态数组,它有 长度(len) 和 容量(cap)。当我们向切片添加元素时,切片的长度会增加。如果长度超出了当前的容量,Go 会自动扩容。切片扩容的过程是通过 append 函数来触发的。

append 函数的实现

append 函数是 Go 内置的函数,用于向切片追加元素。它的实现会首先检查当前切片的容量是否足够容纳新的元素,如果不够,它会调用 runtime.growslice 函数来扩容。

// append() 源码简化版
func append(slice []T, elems ...T) []T {
    // 1. 检查当前切片是否有足够的容量
    if len(slice)+len(elems) <= cap(slice) {
        // 如果容量足够,直接追加元素
        slice = slice[:len(slice)+len(elems)]
        copy(slice[len(slice)-len(elems):], elems)
        return slice
    }
    // 2. 如果容量不足,进行扩容
    return growslice(reflect.TypeOf(slice).Elem(), slice, len(slice)+len(elems))
}

2. runtime.growslice 函数

当切片容量不足时,append 会调用 growslice 函数来扩容。growslice 函数是 Go 运行时内部函数,负责实际的内存分配和切片扩容操作。

growslice 函数源码

// growslice 扩容的实现
func growslice(et *_type, old slice, cap int) slice {
    if cap < old.cap {
        panic("growslice: cap out of range")
    }

    // 如果元素大小为 0,直接返回新的切片
    if et.size == 0 {
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

    // 扩容策略:根据当前容量和预期的新容量来决定新容量
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        const threshold = 256
        if old.cap < threshold {
            newcap = doublecap
        } else {
            for 0 < newcap && newcap < cap {
                newcap += (newcap + 3*threshold) / 4
            }
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    // 根据扩容后的容量计算所需的内存空间
    var lenmem, newlenmem, capmem uintptr
    switch {
    case et.size == 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case et.size == goarch.PtrSize:
        lenmem = uintptr(old.len) * goarch.PtrSize
        newlenmem = uintptr(cap) * goarch.PtrSize
        capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
        newcap = int(capmem / goarch.PtrSize)
    case isPowerOfTwo(et.size):
        var shift uintptr
        if goarch.PtrSize == 8 {
            shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
        } else {
            shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
        }
        lenmem = uintptr(old.len) << shift
        newlenmem = uintptr(cap) << shift
        capmem = roundupsize(uintptr(newcap) << shift)
        newcap = int(capmem >> shift)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem, _ = math.MulUintptr(et.size, uintptr(newcap))
        capmem = roundupsize(capmem)
        newcap = int(capmem / et.size)
    }

    // 分配新的内存,并拷贝旧的数据
    var p unsafe.Pointer
    if et.ptrdata == 0 {
        p = mallocgc(capmem, nil, false)
    } else {
        p = mallocgc(capmem, et, true)
    }

    memmove(p, old.array, lenmem)
    return slice{p, old.len, newcap}
}

3. 扩容的具体过程

  • 检查容量是否足够: growslice 首先会检查请求的扩容是否超过了当前切片的容量。如果新容量大于原容量的两倍,它会直接使用新容量作为目标容量。

  • 根据扩容策略计算新容量:

    • 如果切片的容量小于 256,它会直接翻倍容量(newcap = cap)。

    •  如果切片容量较大,扩容时会采取逐步增加的策略:按原容量的 1.25 倍+192 (也就是3*threshold / 4) 增长,直到满足目标容量。

  • 计算内存所需大小: growslice 会计算新切片所需的内存大小,并分配新的内存空间。对于不同类型的元素(如基本类型、指针类型等),内存分配的方式会有所不同。

  • 内存分配和数据拷贝:

    • 通过 mallocgc 分配新的内存区域,在分配空间的时候因根据标签的大小等级会向上取整,go在扩容的时候宁愿牺牲一部分内部碎片也要保证外部尽可能少的外部碎片,并使用 memmove 将原切片的数据拷贝到新的内存中。

    • 扩容后的切片会更新 指针(指向新的内存)、长度 和 容量,然后返回新的切片。

4. 扩容的性能考虑

切片扩容的过程需要进行 内存分配 和 数据拷贝,这会影响性能,尤其是在频繁扩容的场景中。

  • 频繁扩容的开销:每次扩容时,都会重新分配内存并将数据拷贝到新的内存区域,这会带来额外的时间和空间开销。

  • 容量预估:为了避免频繁扩容,最好在创建切片时预估切片需要的容量,尤其是在元素数量已知的情况下。可以使用 make([]T, 0, n) 来预分配一个足够的容量,以减少扩容操作。

5. 扩容策略的总结

  • 小容量时(比如容量小于 256),切片会按 2 倍扩容。

  • 大容量时,Go 会使用 1.25 倍扩容 + 192 的策略,确保不会过度浪费内存。

  • 对于 nil 或零容量切片,growslice 会初始化并分配合适的容量。

6. 示例:切片扩容过程

下面是一个示例,展示了切片扩容的过程:

package main

import "fmt"

func main() {
    var slice []int
    for i := 0; i < 10; i++ {
        slice = append(slice, i)
        fmt.Printf("Length: %d, Capacity: %d, Slice: %v\n", len(slice), cap(slice), slice)
    }
}

输出示例:

Length: 1, Capacity: 1, Slice: [0]
Length: 2, Capacity: 2, Slice: [0 1]
Length: 3, Capacity: 4, Slice: [0 1 2]
Length: 4, Capacity: 4, Slice: [0 1 2 3]
Length: 5, Capacity: 8, Slice: [0 1 2 3 4]
Length: 6, Capacity: 8, Slice: [0 1 2 3 4 5]
Length: 7, Capacity: 8, Slice: [0 1 2 3 4 5 6]
Length: 8, Capacity: 8, Slice: [0 1 2 3 4 5 6 7]
Length: 9, Capacity: 16, Slice: [0 1 2 3 4 5 6 7 8]
Length: 10, Capacity: 16, Slice: [0 1 2 3 4 5 6 7 8 9]

如上所示,切片的容量在每次添加元素时会根据扩容策略进行增长。初始容量为 1,当容量不足时,扩容后容量翻倍。最终,容量达到 16。

总结

Go 的切片扩容机制是通过 append 函数和 growslice 函数实现的。在扩容时,Go 会根据当前切片的容量和预期的容量进行计算,并分配新的内存空间,拷贝原有数据。扩容的策略通常是小容量时翻倍,大容量时逐步增加,保证了内存的高效使用。

到此这篇关于golang中slice扩容的具体实现的文章就介绍到这了,更多相关golang slice扩容内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go 简单实现多租户数据库隔离

    Go 简单实现多租户数据库隔离

    本文主要介绍了Go 简单实现多租户数据库隔离,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • Makefile构建Golang项目示例详解

    Makefile构建Golang项目示例详解

    这篇文章主要为大家介绍了Makefile构建Golang项目的过程示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • gorm golang 并发连接数据库报错的解决方法

    gorm golang 并发连接数据库报错的解决方法

    今天小编就为大家分享一篇gorm golang 并发连接数据库报错的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-07-07
  • go内存队列list VS slice实现方式对比分析

    go内存队列list VS slice实现方式对比分析

    这篇文章主要为大家介绍了go内存队列list VS slice实现方式对比分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • Go语言的匿名字段实现组合复用实例探究

    Go语言的匿名字段实现组合复用实例探究

    这篇文章主要为大家介绍了Go语言的匿名字段实现组合复用实例探究,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • golang调用shell命令(实时输出,终止)

    golang调用shell命令(实时输出,终止)

    本文主要介绍了golang调用shell命令(实时输出,终止),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-02-02
  • Go语言学习教程之goroutine和通道的示例详解

    Go语言学习教程之goroutine和通道的示例详解

    这篇文章主要通过A Tour of Go中的例子进行学习,以此了解Go语言中的goroutine和通道,文中的示例代码讲解详细,感兴趣的可以了解一下
    2022-09-09
  • go语言实现依赖注入的示例代码

    go语言实现依赖注入的示例代码

    依赖注入和控制反转恰恰相反,它是一种具体的编码技巧,我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递给类来使用,本文将给大家介绍go语言实现依赖注入,需要的朋友可以参考下
    2024-01-01
  • Go 的入口函数和包初始化的使用

    Go 的入口函数和包初始化的使用

    本文主要介绍了Go 的入口函数和包初始化的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • Go panic的三种产生方式细节探究

    Go panic的三种产生方式细节探究

    这篇文章主要介绍了Go panic的三种产生方式细节探究,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12

最新评论