You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Go中4k个10万长度切片比同规格数组内存占用低?

Go中数组与切片作为Map值的内存差异原因

测试场景

场景1:存储固定长度数组的Map

执行以下代码创建包含4000个长度为100_000的int数组的Map:

parentMap := make(map[int][100_000]int)
for i := 0; i < 4000; i++ {
    parentMap[i] = [100_000]int{}
    time.Sleep(3 * time.Millisecond)
}

本地运行后内存占用超过2GB。

场景2:存储切片的Map

修改代码为存储长度为100_000的int切片的Map:

parentMap := make(map[int][]int)
for i := 0; i < 4000; i++ {
    parentMap[i] = make([]int, 100_000)
    time.Sleep(3 * time.Millisecond)
}

本机运行时内存峰值约为73MB。

提问者的困惑

提问者原本认为两者内存占用应大致相当,理由如下:

  • 两种场景下Go运行时都会将parentMap的值分配在堆上,避免函数退出后被回收;
  • 场景1直接在堆上分配4000个数组;
  • 场景2在堆上分配4000个切片头,每个切片头指向堆上独立的10万长度数组;
  • 两者堆上都包含4000个10万长度的数组,内存占用应相近。

查阅Go官方文档《Slices: usage and internals》后未找到相关解释,寻求差异原因。

核心原因解析

差异的本质是Go中数组是值类型,切片是引用类型,且Map的赋值逻辑放大了这一特性:

  1. 数组作为Map值的内存开销
    数组是值类型,赋值操作会触发完整的元素复制。当执行parentMap[i] = [100_000]int{}时:

    • 首先会在堆上创建一个全新的10万长度int数组(因为数组过大无法在栈上分配);
    • 然后将这个数组的所有元素完整复制到Map为该键分配的堆内存空间中;
    • 复制完成前,堆上会同时存在两个完整的10万长度数组,直接导致内存占用翻倍。
      此外,Map存储大值类型时,哈希桶的扩容和管理也会产生额外的内存开销,进一步推高内存使用。
  2. 切片作为Map值的内存开销
    切片是引用类型,其本身仅包含3个字段(指向底层数组的指针、长度、容量),总大小仅24字节(64位系统):

    • make([]int, 100_000)直接在堆上创建10万长度的int数组,并返回指向该数组的切片头;
    • 将切片赋值给Map时,仅复制这24字节的切片头到Map的哈希桶中,不会复制底层数组;
    • 堆上仅存在一份底层数组,无额外复制开销,内存占用仅为4000个底层数组的大小加上可忽略的切片头内存。
  3. 关于场景2内存峰值的补充说明
    你观察到的73MB内存峰值远低于理论计算值,大概率是Go运行时的GC优化导致:在循环的time.Sleep间隙,GC会及时回收临时对象,或者对于全零值的底层数组,运行时可能存在复用机制(尽管Go规范未明确保证),进一步降低了实际内存占用。

内容的提问来源于stack exchange,提问作者Dan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 14:54:56