返回

GO 进阶源码分析之 切片扩容原理

后端

Go 切片扩容机制剖析

什么是切片扩容?

Go 中的切片是一种轻量级的动态数组,当切片的长度超过其容量时,需要进行扩容才能存储更多元素。扩容是指分配一个新切片,其容量比原切片更大,并将原切片中的数据复制到新切片中。

扩容机制演变

Go 语言中切片的扩容机制随着版本的更新而不断演变。在 Go 1.18 版本之前和之后,切片的扩容行为存在差异。

Go 1.18 之前的扩容机制

在此版本中,切片的扩容行为如下:

  • 切片容量小于 1024 时,扩容时容量会翻倍。
  • 切片容量大于或等于 1024 时,扩容时容量会增加 1.25 倍。
  • 扩容后,切片的容量会向上取整到最近的 2 的幂。

Go 1.18 之后的扩容机制

从 Go 1.18 版本开始,切片的扩容行为有所改变:

  • 切片容量小于 1024 时,扩容时容量会翻倍。
  • 切片容量大于或等于 1024 时,扩容时容量会增加 2 倍。
  • 扩容后,切片的容量会向上取整到最近的 2 的幂。

扩容源码分析

Go 1.18 之前

func growslice(et *_type, old []T, cap int) (array unsafe.Pointer) {
    if cap < 1024 {
        newcap := oldcap * 2 // double old cap
        if newcap < cap {
            newcap = cap
        }
    } else {
        newcap = oldcap * 5 / 4 // add 25%, never less than 1024
        if newcap < cap {
            newcap = cap
        }
    }
    return newarray(et, newcap)
}

从代码中可以看到,当切片容量小于 1024 时,扩容时容量会翻倍。当切片容量大于或等于 1024 时,扩容时容量会增加 1.25 倍。

Go 1.18 之后

func growslice(et *_type, old []T, cap int) (array unsafe.Pointer) {
    if cap < 1024 {
        newcap := oldcap * 2 // double old cap
        if newcap < cap {
            newcap = cap
        }
    } else {
        newcap = oldcap * 2 // double old cap
        if newcap < cap {
            newcap = cap
        }
    }
    return newarray(et, newcap)
}

从代码中可以看到,在 Go 1.18 版本中,当切片容量小于 1024 时,扩容时容量会翻倍。当切片容量大于或等于 1024 时,扩容时容量会增加 2 倍。

注意事项

使用 Go 切片时需要注意以下几点:

  • 扩容可能会导致数据复制,因此避免频繁的扩容操作。
  • 扩容后,原切片的数据仍然存在,只是容量变大。
  • 扩容后,原切片的长度不会改变。

最佳实践

为了避免频繁的切片扩容操作,可以考虑以下最佳实践:

  • 预分配切片的容量,尤其是在知道切片需要存储的数据量时。
  • 当需要多次追加元素到切片时,预分配切片的容量。
  • 当需要对切片进行频繁的排序或搜索操作时,预分配切片的容量。

常见问题解答

  1. 扩容操作的开销是什么?
    扩容操作涉及分配新切片、复制数据和释放旧切片,因此会产生一些开销。

  2. 为什么切片的容量是 2 的幂?
    2 的幂可以简化容量计算和内存管理,提高切片的性能。

  3. 如何检测切片是否需要扩容?
    当切片的长度超过其容量时,切片需要扩容。可以通过比较切片的 lencap 属性来检测是否需要扩容。

  4. 扩容后旧切片中的数据是否会被释放?
    旧切片中的数据不会被释放,直到旧切片不再被引用。

  5. 如何减少切片的扩容次数?
    通过预分配切片的容量,避免频繁的追加元素操作,并使用切片池等技术可以减少切片的扩容次数。

结论

切片的扩容机制是 Go 中一项重要的特性,它允许切片动态调整其容量以适应不断变化的数据需求。了解切片的扩容机制有助于优化代码性能,避免不必要的开销。通过遵循最佳实践和避免频繁的扩容操作,可以充分利用 Go 切片的强大功能。