Golang Slice深度解析

Go 语言 Slice 实现原理深度解析

1. 前言

Go 中的 Slice 又称之为动态数组,依托数组实现,可以方便地进行扩容、传递等等。正因为灵活,如果不了解其中实现机制,在生产实际中避免不了出一些奇怪的 BUG。

2. Slice 实现原理

Slice 依托数组实现,底层对用户屏蔽,在底层数组容量不足时候,可以自动重分配并生成新的 Slice,源码中定义的结构如下:

go 复制代码
type slice struct {
    array unsafe.Pointer       // 指向数组的指针
    len   int                  // 数组长度
    cap   int                  // 数组容量
}

3. Slice 的创建

3.1 使用 make 创建 slice

使用 make 创建 slice 时,可以指定 slice 的长度与容量,也就是会创建一个底层数组,数组的长度即容量。

如下图所示,使用 make([]int, 5, 10) 创建 slice 的底层结构:

3.2 使用数组创建 slice

使用数组创建 slice 的话,slice 将与原数组共用一部分内存。

如下图所示,使用 array[5:7] 创建的 slice 结构:

注意:切片与原来数组共享底层内存

4. Slice 的扩容

使用 append 向一个 slice 添加新的元素时候,避免不了原数组容量大小不够,此时会触发 slice 的扩容机制,重新分配一个更大内存的底层数组,将原来 slice 数据拷贝进新的数组,然后返回新的 slice,再次 append 数据。

源代码如下:

go 复制代码
// oldPtr: 指向旧的数组指针
// newLen: 扩容后的需要长度
// oldCap: 旧的数组长度
// num: 新增元素的数量
// et: 元素类型信息
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
    oldLen := newLen - num

    if newLen < 0 {
        panic(errorString("growslice: len out of range"))
    }

    if et.Size_ == 0 {
        // append should not create a slice with nil pointer but non-zero len.
        // We assume that append doesn't need to preserve oldPtr in this case.
        return slice{unsafe.Pointer(&zerobase), newLen, newLen}
    }
    // 分配新的容量
    newcap := nextslicecap(newLen, oldCap)

    ....... // 一些内存对齐的优化操作

    var p unsafe.Pointer
    // 判断元素类型是否包含指针
    if !et.Pointers() {
        // 分配新内存
        p = mallocgc(capmem, nil, false)
        // The append() that calls growslice is going to overwrite from oldLen to newLen.
        // Only clear the part that will not be overwritten.
        // The reflect_growslice() that calls growslice will manually clear
        // the region not cleared here.
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        // 分配新内存
        // Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
        p = mallocgc(capmem, et, true)
        // 此处开启了写屏障,避免旧的数组被提前回收
        if lenmem > 0 && writeBarrier.enabled {
            // Only shade the pointers in oldPtr since we know the destination slice p
            // only contains nil pointers because it has been cleared during alloc.
            //
            // It's safe to pass a type to this function as an optimization because
            // from and to only ever refer to memory representing whole values of
            // type et. See the comment on bulkBarrierPreWrite.
            bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
        }
    }
    // 拷贝旧数据
    memmove(p, oldPtr, lenmem)
    // 返回新的slice
    return slice{p, newLen, newcap}
}

4.1 扩容容量计算

关于如何计算新的 slice 长度,源码中这样写道:

go 复制代码
// nextslicecap computes the next appropriate slice length.
// newLen: 扩容后需要的最小数组长度
// oldCap: 扩容前的当前容量
func nextslicecap(newLen, oldCap int) int {
    newcap := oldCap
    doublecap := newcap + newcap
    // 如果一次性要分配的元素数量非常多,则按需分配
    if newLen > doublecap {
        return newLen
    }

    const threshold = 256
    // 小容量直接翻倍
    if oldCap < threshold {
        return doublecap
    }
    // 大容量渐进增长
    for {
        newcap += (newcap + 3*threshold) >> 2
        // 直到newCap >= newLen
        // 主要是为了防止过度分配,大对象尽量节省内存
        if uint(newcap) >= uint(newLen) {
            break
        }
    }

    // Set newcap to the requested cap when
    // the newcap calculation overflowed.
    // 溢出判断
    if newcap <= 0 {
        return newLen
    }
    return newcap
}

一个简单的图片示意图:

5. Slice 总结

  • 每个切片都指向一个底层数组
  • 每个切片都保存了当前切片的长度、底层数组可用容量
  • 使用 len() 计算切片长度时间复杂度为 O(1),不需要遍历切片
  • 使用 cap() 计算切片容量时间复杂度为 O(1),不需要遍历切片
  • 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而已
  • 使用 append() 向切片追加元素时有可能触发扩容,扩容后将会生成新的切片
相关推荐
荒诞硬汉2 分钟前
面向对象(三)
java·开发语言
郝学胜-神的一滴3 分钟前
深入理解Linux中的Try锁机制
linux·服务器·开发语言·c++·程序人生
liliangcsdn3 分钟前
bash中awk如何切分输出
开发语言·bash
柒.梧.5 分钟前
Spring Boot集成JWT Token实现认证授权完整实践
java·spring boot·后端
菜鸟233号9 分钟前
力扣416 分割等和子串 java实现
java·数据结构·算法·leetcode
csbysj202010 分钟前
JSON.parse() 方法详解
开发语言
奔波霸的伶俐虫12 分钟前
redisTemplate.opsForList()里面方法怎么用
java·开发语言·数据库·python·sql
yesyesido23 分钟前
智能文件格式转换器:文本/Excel与CSV无缝互转的在线工具
开发语言·python·excel
_200_25 分钟前
Lua 流程控制
开发语言·junit·lua
环黄金线HHJX.26 分钟前
拼音字母量子编程PQLAiQt架构”这一概念。结合上下文《QuantumTuan ⇆ QT:Qt》
开发语言·人工智能·qt·编辑器·量子计算