跳到主要内容

Go 切片缩容

· 阅读需 3 分钟
素明诚
Full stack development

在 Go 中,由于切片底层使用数组实现,真正的缩容通常指的是减少切片的容量,以释放不需要的内存。这通常通过创建一个新的切片并复制所需数据来实现。

创建新切片并拷贝数据

通过创建一个新的切片,并且将旧切片的数据复制到这个新的切片中。新切片的容量和长度被精确控制。

注意,这里使用make([]Type, len, cap)创建新的切片时,Go 运行时会分配一个全新的底层数组。

这种方式是在你永远不会让这个切片再扩容的时候使用。

func ShrinkSlice(original []int, newLength int) []int {
if newLength > len(original) {
newLength = len(original)
}
newSlice := make([]int, newLength, newLength) // 设置新切片的长度和容量
copy(newSlice, original[:newLength])
return newSlice
}

利用 append 函数

append 函数可以在增加切片长度的同时控制其容量。如果在使用 append 时开始一个新的切片(nil 切片),Go 的编译器和运行时会优化内存分配,通常只会分配到所需的容量。

这里append函数在添加元素超过原始切片容量时会自动分配新的底层数组,并复制原始数据到新数组。即使开始时指定了容量,append操作仍可能触发新数组的创建,这是为了保证切片的扩展不会影响到其他依赖原数组的切片。

func ShrinkSliceUsingAppend(original []int, newLength int) []int {
if newLength > len(original) {
newLength = len(original)
}
newSlice := make([]int, 0, newLength) // 创建一个容量为newLength的空切片
newSlice = append(newSlice, original[:newLength]...)
return newSlice
}

使用全局切片重新切片

如果不担心原始数据的保护,可以通过重新切片(reslicing)操作直接在原切片上操作,这种方法不需要额外的内存分配,但是需要手动管理原始数据。(使用全局切片重新切片)会保持与原始底层数组的直接联系,而前两种方法都会创建一个新的底层数组。

func ShrinkSliceInPlace(original []int, newLength int) []int {
if newLength > len(original) {
newLength = len(original)
}
return original[:newLength:newLength] // 设置新切片的长度和容量相同
}

缩容后,尽量少的使用 CPU

整个实现的核心是希望在后续少触发扩容的前提下,一次性释放尽可能多的内存

func calCapacity(c, l int) (int, bool) { // usage = Deng Ming
// 容量 <=64 缩不缩都无所谓, 因为浪费内存也浪费不了多少
// 你可以考虑调大这个阈值, 或者调小这个阈值
if c <= 64 {
return c, false
}
// 如果容量大于 2048, 但是元素不足一半,
// 降低为 0.625, 也就是 5/8
// 也就是比一半多一点, 和正向扩容的 1.25 倍相呼应
if c > 2048 && (c/l >= 2) {
factor := 0.625
return int(float32(c) * float32(factor)), true
}
// 如果在 2048 以内, 并且元素不足 1/4, 那么直接缩减为一半
if c <= 2048 && (c/l >= 4) {
return c / 2, true
}
return c, false
}