您现在的位置是:首页 >技术杂谈 >Go Slice(切片)原理及扩容机制解析网站首页技术杂谈
Go Slice(切片)原理及扩容机制解析
Go 切片原理及扩容机制解析
在 Go 语言中,切片(slice)作为一种灵活且高效的数据结构,广泛应用于日常开发。本文将深入解析 Go 切片的底层结构、初始化方式、共享机制以及扩容策略,并结合源码分析其内部实现细节,帮助你更好地理解和使用切片。
1. Go 切片的底层结构
Go 切片底层由一个结构体实现,该结构体包含指向底层数组的指针、切片的长度以及容量。其定义大致如下:

type slice struct {
// 指向底层数组起始位置的指针
array unsafe.Pointer
// 当前切片的长度
len int
// 底层数组的容量
cap int
}
2. 切片的初始化
Go 中可以通过多种方式初始化切片,常见的有以下几种方式:
2.1 直接声明
此时声明的切片为 nil 切片,不能直接使用其元素:
var s []int
2.2 使用 make 初始化
使用 make 函数可以创建指定长度和容量的切片:
-
创建长度和容量均为 8 的切片:
s := make([]int, 8)此时,切片中每个位置都已经被分配了默认值。
-
指定长度为 8、容量为 16 的切片:
s := make([]int, 8, 16)注意:可以访问
[0, len)区间的元素,而访问[len, cap)的位置会触发 panic。
2.3 使用字面量进行初始化
直接通过字面量创建并赋值:
s := []int{1, 2, 3}
3. 切片的共享底层数组
需要注意的是,切片本身只是对底层数组的一个描述。当你对切片进行截取操作时,新切片仍然引用原有的底层数组,这意味着修改新切片的元素会影响原切片。
示例代码
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3, 4, 5}
s1 := s[1:]
fmt.Println("s 的首地址: ", &s[0])
fmt.Println("s1 的首地址: ", &s1[0])
s1[0] = -1
fmt.Println("修改后的 s: ", s)
}
输出示例:
s 的首地址: 0x1400012e000
s1 的首地址: 0x1400012e008
修改后的 s: [1 -1 3 4 5]
如上所示,虽然 s 与 s1 是两个不同的切片,但它们共享同一块底层数组:

4. Go 切片的扩容机制
当使用 append() 向切片中添加新元素时,如果切片的容量不足,Go 会自动扩容。扩容并非“刚好分配所需空间”,而是遵循一套优化策略,平衡性能和内存使用。下面将详细介绍扩容的原理和实现。
4.1 扩容预期
假设初始切片 s1 的容量为 3:
s1 := make([]int, 3, 3)
如果使用 append() 添加 3 个新元素:
s1 = append(s1, 1, 2, 3)
此时,切片的预期长度 newLen 为 6,底层数组必须扩容以容纳至少 6 个元素。
4.2 扩容策略概览
Go 的扩容策略主要分为三种情况:
情况 1:newLen 超过 oldCap 的两倍
当所需长度远大于当前容量时,Go 会直接将 newLen 作为新的容量,以减少多次扩容和数据拷贝的开销。
示例:
s := make([]int, 2, 2)
s = append(s, make([]int, 10)...)
// 此时 newCap = 12(直接取 newLen)
情况 2:newLen 小于等于 oldCap * 2
此时需要根据原容量大小采用不同的扩容策略:
-
当旧容量小于 256:
直接将容量翻倍,即newCap = oldCap * 2。示例:
s := make([]int, 100, 100) s = append(s, make([]int, 50)...) // newCap = 200 -
当旧容量大于等于 256:
采用渐进扩容,约 1.25 倍增长,避免一次性分配过多内存。其中,使用768 = 3 × 256作为经验值,使扩容过程更平滑。示例:
s := make([]int, 300, 300) s = append(s, make([]int, 50)...) // newCap = 300 + (300 + 768) / 4 ≈ 525
情况 3:防止溢出
如果在扩容计算过程中出现溢出(例如计算结果为负数),Go 则直接将 newLen 作为新的容量,防止扩容失败。
4.3 growslice() 的内部实现
Go 的 runtime 包中,growslice() 函数负责处理切片的扩容逻辑,其主要流程包括:
- 计算新容量:依据扩容规则确定新的容量(
newCap)。 - 分配新的底层数组:根据新容量分配内存。
- 数据拷贝:将原数组数据拷贝到新的底层数组中。
- 返回新切片:扩容后,新切片与原切片不再共享底层数组。
示例说明:
s1 := []int{1, 2, 3}
s2 := append(s1, 4, 5, 6)
// 此时 s1 与 s2 使用的是不同的底层数组
下面是 Go runtime 包中 growslice() 及辅助函数 nextslicecap() 的部分源码,展示了其扩容机制的具体实现:
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
// 计算旧切片长度
oldLen := newLen - num
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(oldPtr, uintptr(oldLen*int(et.Size_)), callerpc, abi.FuncPCABIInternal(growslice))
}
if msanenabled {
msanread(oldPtr, uintptr(oldLen*int(et.Size_)))
}
if asanenabled {
asanread(oldPtr, uintptr(oldLen*int(et.Size_)))
}
if newLen < 0 {
panic(errorString("growslice: len out of range"))
}
if et.Size_ == 0 {
// 对于 size 为 0 的类型(如 struct{}),返回一个 zerobase 实例
return slice{unsafe.Pointer(&zerobase), newLen, newLen}
}
newcap := nextslicecap(newLen, oldCap)
var overflow bool
var lenmem, newlenmem, capmem uintptr
// 针对常见的 et.Size_ 值进行特殊处理,提高运算效率
noscan := !et.Pointers()
switch {
case et.Size_ == 1:
lenmem = uintptr(oldLen)
newlenmem = uintptr(newLen)
capmem = roundupsize(uintptr(newcap), noscan)
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.Size_ == goarch.PtrSize:
lenmem = uintptr(oldLen) * goarch.PtrSize
newlenmem = uintptr(newLen) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize)
case isPowerOfTwo(et.Size_):
var shift uintptr
if goarch.PtrSize == 8 {
shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63
} else {
shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31
}
lenmem = uintptr(oldLen) << shift
newlenmem = uintptr(newLen) << shift
capmem = roundupsize(uintptr(newcap)<<shift, noscan)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
capmem = uintptr(newcap) << shift
default:
lenmem = uintptr(oldLen) * et.Size_
newlenmem = uintptr(newLen) * et.Size_
capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
capmem = roundupsize(capmem, noscan)
newcap = int(capmem / et.Size_)
capmem = uintptr(newcap) * et.Size_
}
if overflow || capmem > maxAlloc {
panic(errorString("growslice: len out of range"))
}
var p unsafe.Pointer
if !et.Pointers() {
p = mallocgc(capmem, nil, false)
// 清理不会被覆盖的内存区域
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
}
}
memmove(p, oldPtr, lenmem)
return slice{p, newLen, newcap}
}
// 计算新容量的辅助函数
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 {
// 从小切片直接翻倍逐渐过渡到大切片采用约 1.25 倍增长
newcap += (newcap + 3*threshold) >> 2
if uint(newcap) >= uint(newLen) {
break
}
}
if newcap <= 0 {
return newLen
}
return newcap
}
4.4 扩容策略的优势
直接按照 newLen 分配内存虽然能精确匹配需求,但会导致频繁扩容和数据拷贝,从而影响性能。Go 采用预先扩容策略,有以下优势:
-
减少扩容次数
较大的扩容比例(小切片翻倍、大切片 1.25 倍)能显著降低扩容次数,从而提高append()的性能。 -
优化内存管理
Go 的内存分配器mallocgc通常按 2 的幂次对齐内存块,提前扩容可以使最终分配的内存更符合对齐要求,进一步提升效率。
5. 总结
- 切片结构:Go 的切片由指向底层数组的指针、长度和容量构成,其数据实际存储在底层数组中。
- 初始化方式:支持声明、
make以及字面量初始化,开发者可根据具体需求选择合适的方式。 - 共享机制:切片的截取操作不会复制底层数组,而是共享同一块内存,这一点在修改数据时需要特别注意。
- 扩容策略:当容量不足时,Go 采用小切片翻倍和大切片约 1.25 倍增长的策略,既减少了扩容次数,又能充分利用内存对齐优势,从而在性能与内存使用间取得平衡。
希望本文能帮助你深入理解 Go 切片的底层实现和扩容机制,为实际开发提供理论支持与实践指导。如果有任何疑问或建议,欢迎在评论区讨论!





QT多线程的5种用法,通过使用线程解决UI主界面的耗时操作代码,防止界面卡死。...
U8W/U8W-Mini使用与常见问题解决
stm32使用HAL库配置串口中断收发数据(保姆级教程)
分享几个国内免费的ChatGPT镜像网址(亲测有效)
Allegro16.6差分等长设置及走线总结