跳到主要内容

Go 里的 slice 和 map,先吃透零值和共享底层数组再写业务

· 阅读需 3 分钟
一介布衣
全栈开发者 / 技术写作者

Go 初学阶段很容易把 slice 和 map 当成“比数组和字典好用一点的容器”。真正开始写业务后,很多让人困惑的 bug 其实都和这两个基础类型有关:为什么函数里改了 slice,外面数据也变了;为什么 map 还没赋值就 panic;为什么看起来只是截了个子切片,内存却迟迟下不来。

零值能不能直接用,slice 和 map 不一样

slice 的零值是 nil,但很多情况下可以直接 append,这也是它顺手的地方。map 就不一样了,零值 map 只能读,不能直接写。很多初始化遗漏导致的 panic,都是因为默认以为“既然都是引用型用法,应该都能先声明再写”。

把这个差异吃透以后,很多初始化代码就会更自然。一个字段到底该保持 nil,还是要尽早 make 出来,其实也会影响后面的序列化、判空和默认行为。

slice 好用的代价,是它可能共享底层数组

slice 最方便的地方是切一段出来几乎没成本,但这也意味着它可能和原来的数据共享底层数组。你以为只是把一段结果传给下游继续处理,下游却在修改过程中把原始数据也改了,这类问题排查起来非常绕。

所以只要某个 slice 会跨边界传递,而且下游可能修改它,我会更愿意明确做一次拷贝。多一行复制代码,通常比后面追共享数据副作用省事得多。

append 之后的行为,要先想“它是不是还指向原来那块底层数组”

append 最容易让人误判的地方,是有时会原地扩,有时会重新分配。容量够时,原始 slice 和追加后的 slice 可能还共用同一块内存;容量不够时,又会悄悄换到新地址。也就是说,同样一段代码,在不同输入规模下表现可能不完全一样。

这正是很多“测试没复现,线上却撞上”的原因。因为测试数据量小,append 没触发重新分配;线上数据一大,底层行为就变了。遇到这种场景,靠猜很难稳定,最好一开始就把是否共享、是否需要复制想清楚。

写业务之前先对数据边界有感觉

slice 和 map 本身并不复杂,难的是它们太常用了,稍微理解不牢就会渗进很多地方。我现在更愿意把它们看成“需要边界意识的容器”:什么时候允许 nil,什么时候必须初始化,什么时候能共享,什么时候一定要复制。

这些基础习惯不会让代码立刻更炫,但会让数据流稳定很多。Go 的很多实践经验,最后其实都落在这种看似简单、却特别影响日常开发的细节上。