我以为 append 是复制,结果改了原数组:Go 切片的底层数组共享与扩容规则
个人主页 我不会起名字322 欢迎各位大佬莅临其他栏目 技术栈学习笔记 其他栏目 力扣Hot100题目解析 其他栏目 Go项目学习笔记 其他栏目 redis 其他栏目 mysql 文章目录我以为 append 是复制结果改了原数组Go 切片的底层数组共享与扩容规则先说结论我遇到的事为什么切片其实是个三字段的结构体反直觉一子切片和原切片共用一块内存反直觉二append 有时改原数组有时不改顺带一个容易误解的点扩容规则到底怎么算三种让子切片真正独立的办法一个更隐蔽的问题截取大切片会让内存释放不掉排查速查表小结我以为 append 是复制结果改了原数组Go 切片的底层数组共享与扩容规则摘要记录我第一次遇到 append 改掉原切片时的懵圈过程。讲清切片的三字段结构、子切片为什么共享底层数组、append 在什么条件下就地写入、什么条件下触发扩容并彻底解耦以及三种让子切片真正独立的写法copy、三索引表达式、借 nil 扩容。最后补一个更隐蔽的坑从大切片里截一小段会让整块内存释放不掉。文中所有结论都配了可运行代码和输出建议对着跑一遍。标签go · golang · 切片 · slice · append专栏Go 学习笔记先说结论切片不是数组它是一个{指针, 长度, 容量}的三字段描述符本身不存数据。用s[i:j]得到的子切片和原切片共用同一块底层数组改一个另一个跟着变。append时如果len cap会就地写入底层数组——这就是改了原数组的原因只有len cap触发扩容才会分配新数组和原切片彻底解耦。想让子切片独立用copy、用三索引表达式s[i:j:j]把容量卡死或者append([]T(nil), s...)。我遇到的事那天我在写一个函数想把切片的前两个元素取出来单独处理funcmain(){a:[]int{1,2,3,4,5}b:a[1:3]fmt.Println(b ,b)}输出b [2 3]一切正常。然后我往b里追加了一个元素funcmain(){a:[]int{1,2,3,4,5}b:a[1:3]bappend(b,99)fmt.Println(b ,b)fmt.Println(a ,a)}我预期a还是[1 2 3 4 5]。实际输出是b [2 3 99] a [1 2 3 99 5]a的第四个元素被改成了 99。我压根没碰过a。更让人崩溃的是在后面——我把截取范围改成a[1:4]同样的appenda又没被改。同一个操作两次结果不一样。这种时灵时不灵的 bug 最折磨人所以我把底层机制完整捋了一遍。为什么切片其实是个三字段的结构体Go 的切片在运行时长这样reflect.SliceHeadertypeslicestruct{data unsafe.Pointer// 指向底层数组的指针lenint// 当前长度能访问的元素个数capint// 容量从 data 起算底层数组还剩多少位置}注意data是个指针。切片自己不持有数据它只是底层数组上的一个窗口。关键在len和cap的区别a:[]int{1,2,3,4,5}fmt.Println(len(a),cap(a))// 5 5b:a[1:3]fmt.Println(len(b),cap(b))// 2 4b的长度是 2元素[2 3]但容量是 4。因为b.data指向的是a[1]从那个位置往后数底层数组还剩 4 个位置a[1]到a[4]底层数组 [1] [2] [3] [4] [5] ↑ ↑ b.data b 的容量边界 |---- cap4 ----| |- len2 -|容量大于长度意味着窗口后面还有看不见但存在的空间。这就是所有问题的根源。反直觉一子切片和原切片共用一块内存切片操作a[i:j]不会复制任何数据它只是创建一个新的描述符。所以a:[]int{1,2,3,4,5}b:a[1:3]b[0]999fmt.Println(a)// [1 999 3 4 5]fmt.Println(b)// [999 3]连append都不用直接改元素就已经互相影响了。这个还比较好理解。真正隐蔽的是append的行为。反直觉二append 有时改原数组有时不改append的逻辑其实只有两条分支条件行为对原切片的影响len cap直接写进底层数组的下一个空位原切片被修改len cap分配新数组、拷贝旧数据、再追加原切片不变彻底解耦把前面的例子拆开看。情况一len cap就地写入a:[]int{1,2,3,4,5}b:a[1:3]// len2, cap4bappend(b,99)// 2 4就地写入 a[3]fmt.Println(a)// [1 2 3 99 5] ← 被改了情况二len cap触发扩容a:[]int{1,2,3,4,5}b:a[1:3:3]// 三索引len2, cap2bappend(b,99)// 2 2必须扩容fmt.Println(a)// [1 2 3 4 5] ← 没被改fmt.Println(b)// [2 3 99]三索引表达式a[low:high:max]可以把容量精确限制成max - low。这里cap被卡成 2append立刻触发扩容b拿到一块新数组和a再无关系。所以时灵时不灵的真相是取决于当时容量够不够。而容量够不够又取决于你是怎么截的、以及 append 之前 cap 是多少——它不会写在代码表面这就是为什么这类 bug 难查。顺带一个容易误解的点funcadd(s[]int,vint){sappend(s,v)// 只改了形参 s 这个描述符}funcmain(){a:[]int{1,2,3}add(a,4)fmt.Println(a)// [1 2 3]没有 4}切片是描述符传参传的是这个结构体的副本。append返回的新描述符赋值给了形参s调用方的a毫发无损。除非append触发了扩容底层数组的修改也只发生在形参可见范围内。想让外面看到就得接收返回值a add(a, 4)。扩容规则到底怎么算既然会不会扩容这么关键那容量到底按什么规律增长Go 运行时runtime/slice.go的growslice的规则大致是cap 256直接翻倍newcap oldcap * 2cap 256每次增长约 25%newcap (newcap 3*256) / 4直到能装下新元素最后还有一步内存规格对齐roundupsize把容量向上取整到 Go 的内存尺寸等级第三步会让实际容量比公式算出来的略大所以别硬记具体数字跑一遍最准funcmain(){s:make([]int,0)prev:0fori:0;i2000;i{sappend(s,i)ifcap(s)!prev{fmt.Printf(len%-5d cap%d\n,len(s),cap(s))prevcap(s)}}}在我的环境Go 1.22上输出len1 cap1 len2 cap2 len3 cap4 len5 cap8 len9 cap16 len17 cap32 len33 cap64 len65 cap128 len129 cap256 len257 cap512 len513 cap848 len849 cap1280 len1281 cap1792 len1793 cap2560前 256 是干净的翻倍之后变成 512 → 848 → 1280 → 1792 —— 增长幅度确实是四分之一左右且都不是整数倍就是被内存对齐修过的结果。提醒扩容策略是实现细节不同 Go 版本可能微调。可以拿它估算别拿它做逻辑依赖。三种让子切片真正独立的办法按推荐顺序a:[]int{1,2,3,4,5}// 1. copy —— 最清晰可读性最好b:make([]int,len(a[1:3]))copy(b,a[1:3])// 2. 借 nil 一次性扩容 —— 一行搞定常用b:append([]int(nil),a[1:3]...)// 3. 三索引表达式 —— 卡死容量迫使后续 append 必须扩容b:a[1:3:3]bappend(b,99)三种方式得到的b都和a完全独立改谁都不影响对方。要完全复制一份来用选 1 或 2只是想继续 append 但不污染原切片选 3如果已知最终长度创建时就指定 capmake([]T, 0, n)能省掉所有中间扩容拷贝这是性能上最划算的做法。一个更隐蔽的问题截取大切片会让内存释放不掉这是我踩的第二个坑比第一个隐蔽得多。假设你读了一个 100MB 的文件到切片里只需要前 10 个元素funchead(data[]byte)[]byte{returndata[:10]}返回的切片len10但它的data指针仍指向那块 100MB 底层数组的开头cap依然是 100MB 量级。只要这个返回的小切片还活着GC 就无法回收后面那 100MB。正确做法同样是复制出来funchead(data[]byte)[]byte{out:make([]byte,10)copy(out,data[:10])returnout// 只持有 10 字节的新数组}这类问题在长生命周期的缓存、队列、全局 map 里杀伤力最大——程序看着只用了几 MBRSS 却下不来。排查速查表现象最可能的原因处理没改原切片它却变了子切片共享底层数组 append就地写入copy或三索引s[i:j:j]同一个append有时改有时不改取决于当时len是否等于cap用cap()打点确认函数里append了外面看不到描述符是值传递接收返回值重新赋值内存一直降不下来小切片持有着大底层数组复制出独立小切片想知道容量怎么涨的别猜打印上面那段循环直接跑小结切片的核心就一句话它是底层数组上的一个窗口窗口后面还有多少空间cap决定了append是就地写还是搬家。记住这三条就不会再踩s[i:j]不复制数据共享底层数组append在len cap时就地写会污染所有共享这块数组的切片需要独立就用copy或者用三索引s[i:j:j]把容量卡死。至于扩容倍数——知道它是小于 256 翻倍、之后约涨四分之一、最后内存对齐就够了别去背具体数字。下一篇预告Go map 为什么不能并发读写——那个fatal error背后的扩容与渐进搬迁机制。

相关新闻

pstack+Claude Code:生产环境死锁定位的AI辅助工作流

pstack+Claude Code:生产环境死锁定位的AI辅助工作流

凌晨两点半,工区灯还亮着,订单引擎服务彻底假死。进程没有退出,core dump自然不可能,日志最后一行停在14:21:30,CPU占用也就5%上下来回晃,可所有写接口全都超时。这种状态最折磨人,进程活着却不…

2026/10/9 17:09:46 阅读更多 →
Genjutsu AI:面向真实视频的单目标动态物体替换技术

Genjutsu AI:面向真实视频的单目标动态物体替换技术

1. 项目概述:这不是“换脸”,而是“换物”——Genjutsu AI 的真实定位与价值锚点你有没有试过,把一段朋友在咖啡馆里喝拿铁的视频发到朋友圈,结果发现杯垫上印着竞争对手的logo?或者拍了一条产品测评,镜头扫…

2026/10/9 17:09:46 阅读更多 →
Claude Code Mod 实战:从斜杠命令到 Hook 的终端 AI 定制指南

Claude Code Mod 实战:从斜杠命令到 Hook 的终端 AI 定制指南

1. 为什么一个“默认就能用”的工具还要折腾 Mod:先搞清楚改的是什么如果你打开一个终端工具,第一反应是“它默认能用就行,没必要改”,那这篇可能不太适合你;但如果你跟我一样,总觉得默认配置差点意思&…

2026/10/9 17:09:46 阅读更多 →

最新新闻

pstack诊断Claude本地服务性能瓶颈实战指南

pstack诊断Claude本地服务性能瓶颈实战指南

1. 项目概述:pstack-claude 是什么,它解决什么问题 pstack-claude 这个名字乍看像一个工具组合词,但拆开来看,它其实指向一个非常具体、且在当前开发者圈层中高频出现的实践场景: 用 pstack 工具诊断运行中的 Claude…

2026/10/9 17:46:06 阅读更多 →
WorkBuddy 六大行业应用案例:从电商到知识管理的自动化实践

WorkBuddy 六大行业应用案例:从电商到知识管理的自动化实践

1. 从六个真实场景看 WorkBuddy 的落地逻辑第一次听到 WorkBuddy 这个名字,很多人会下意识觉得它不过是又一个套壳的对话工具。但真正把它接进日常工作流的人会发现,这东西的价值根本不在“聊天”上,而在于它能把散落在不同系统里的动作串成一…

2026/10/9 17:46:06 阅读更多 →
统一管理多个AI编程CLI:kshell架构设计与多工具接入实践

统一管理多个AI编程CLI:kshell架构设计与多工具接入实践

1. 为什么需要统一管理多个 AI 编程 CLI1.1 从单工具到多工具并存的现实困境过去一年里,AI 编程 CLI 工具的数量增长得非常快。我自己的机器上就同时装了六款不同的命令行 AI 编程助手,每一款都有自己的强项:有的擅长长上下文代码理解&#x…

2026/10/9 17:46:06 阅读更多 →
Mac Java反编译工具实战:从jar包到可读源码的完整指南

Mac Java反编译工具实战:从jar包到可读源码的完整指南

简介:这是一份面向 macOS 用户的 Java 反编译工具资源,主要解决在苹果系统下查看与还原 class 字节码的需求,适合需要阅读第三方库源码、排查线上问题或做逆向学习的 Java 开发者与安全研究人员使用。压缩包共 8 个文件,整体约 7.…

2026/10/9 17:46:06 阅读更多 →
如何自定义 rangeslider.js 外观:基于 BEM 类名的 SCSS 滑块主题定制指南

如何自定义 rangeslider.js 外观:基于 BEM 类名的 SCSS 滑块主题定制指南

如何自定义 rangeslider.js 外观:基于 BEM 类名的 SCSS 滑块主题定制指南 【免费下载链接】rangeslider.js 🎚 HTML5 input range slider element jQuery polyfill 项目地址: https://gitcode.com/gh_mirrors/ra/rangeslider.js rangeslider.js 外…

2026/10/9 17:46:06 阅读更多 →
如何正确解读memtest_vulkan测试报告?错误日志逐行解析与显存硬件故障判断

如何正确解读memtest_vulkan测试报告?错误日志逐行解析与显存硬件故障判断

如何正确解读memtest_vulkan测试报告?错误日志逐行解析与显存硬件故障判断 【免费下载链接】memtest_vulkan Vulkan compute tool for testing video memory stability 项目地址: https://gitcode.com/gh_mirrors/me/memtest_vulkan memtest_vulkan 是一款基…

2026/10/9 17:45:05 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →