为什么要尽量少用 Go 里的 unsafe
Posted on Wed 11 February 2026 in Journal
| Abstract | 为什么要尽量少用 Go 里的 unsafe |
|---|---|
| Authors | Walter Fan |
| Category | Journal |
| Version | v1.0 |
| Updated | 2026-02-11 |
| License | CC-BY-NC-ND 4.0 |
短大纲
- 用一个“改了一行 unsafe,线上崩了”的小场景开场,点出:unsafe 不是“高级技巧”,是“逃生舱”,用错就沉船
- unsafe 是什么、Go 为什么保留它:类型系统外的底层操作,FFI、反射实现等离不开
- 少用的三大理由:破坏内存安全、违背 GC/栈假设、可移植性和未来兼容无保证
- 什么时候可以考虑用:与 C 互操作、标准库级性能关键路径、且范围极小、文档和测试拉满
- 明天就能做的 3 件事 + 适用边界 + 一个开放式问题
- 文末附思维导图(含渲染图)
2026-02-11
你见过这种事故吗:某次“小优化”里有人用 unsafe.Pointer 把 []byte 转成 string 省了一次拷贝,上线跑了一阵子没事,结果某次 GC 或 runtime 升级之后,线上开始偶现诡异的 coredump 或错误数据。
查到最后,往往是一行“聪明”的 unsafe 代码,在你不以为意的地方打破了 Go runtime 的假设。
反直觉的一点是:unsafe 包名字就叫 unsafe,不是“不推荐”,而是官方在明说:这里没有安全网。 能用类型系统和标准库解决的事,就别往这里走。
unsafe 是什么?Go 为什么还留着它?
unsafe 是 Go 标准库里的一个特殊包,主要提供:
unsafe.Pointer:可以和任意指针类型互转,相当于 C 里的void*,绕过 Go 的类型检查unsafe.Sizeof/Offsetof/Alignof:看内存布局,做结构体内存级操作时用
Go 的设计哲学是“内存安全 + 简单”。那为什么还要留一扇后门?
因为有些事,在类型系统的“围墙”里确实做不了:
- 和 C 互操作:CGO 里要把 Go 的指针传给 C,或者把 C 的指针在 Go 里用,往往要经过
unsafe.Pointer - 标准库和 runtime 的实现:比如
reflect、sync、某些string/[]byte的零拷贝转换,需要在底层操作内存 - 极少数性能关键路径:在充分测量、范围极小、文档和测试都到位的前提下,有人会用来做零拷贝或自定义布局
所以“尽量少用”不等于“永远不用”,而是:能用安全写法就用安全写法,非要用时,把使用范围、前提和风险写清楚。
少用的三大理由
1) 破坏内存安全,bug 难查
Go 帮你防的是:越界、use-after-free、类型乱用。一旦用 unsafe.Pointer 把指针换来换去,这些保护就都没了。
你可能只是“临时”把一块 []byte 的底层指针转成 *string 用了一下,但:
- 若这块 slice 之后被 re-slice 或扩容,底层数组可能迁移,你手里还捏着旧指针,就变成 use-after-free
- 若 goroutine、栈拷贝、逃逸分析和你想象的不一致,崩溃可能出现在完全无关的代码路径上
这类 bug 往往难以稳定复现,排查成本高,所以除非收益明确且不可替代,否则不碰。
2) 违背 GC 与 runtime 的假设
Go 的 GC 和 runtime 会移动对象、调整栈、管理内存布局。文档里写得很清楚:unsafe.Pointer 的很多用法是“未定义行为”——今天没问题,不代表下一个 Go 版本、另一种 GC 压力下也没问题。
例如:把指向栈上变量的指针传到 C 里、或者长期持有某种“内部指针”而不让 GC 感知,都可能在未来版本里暴雷。你等于是和实现细节绑死了,而不是和语言规范绑在一起。
3) 可移植性与“未来兼容”无保证
unsafe 的文档里很少给你“跨版本、跨平台”的承诺。今天在某架构、某 Go 版本下“看起来没问题”的写法,可能下一个 minor 就崩了。
所以:能放在标准库和少量底层库里、由少数人维护的,就尽量不要散落在业务代码里。 业务代码里尽量少用、最好不用。
什么时候可以考虑用?
只有在下面几条都满足时,才值得考虑:
- 确实没有安全替代方案:例如必须和 C 互操作、或要实现的抽象在现有类型系统里表达不了
- 使用范围极小:集中在一两个包、少量函数里,而不是到处传
unsafe.Pointer - 文档和注释写清楚:为什么必须用、依赖了哪些假设、将来可能怎么被替代
- 有测试和复现路径:包括内存/race 相关测试,CI 里跑
如果是“只是想少一次拷贝”“觉得这里用 unsafe 会更快”,先做 benchmark;多数时候,安全写法已经够快,或者可以通过更好的设计(比如少一次分配、换一种 API)达到目标。
一个跨领域的类比
可以把类型系统想象成游泳池的救生员:他限制你不能在深水区做危险动作,但能大大降低溺水概率。unsafe 相当于你翻出围栏去“野泳”——也许你技术好、水域也熟,一次两次没事,但一旦环境变了(水流、体力、别人推你一把),出事的代价会由你和你的团队一起承担。
所以:不是“禁止野泳”,而是“默认在池子里游;非要野泳,要有预案、有边界、有记录”。
总结
- 尽量少用:因为会破坏内存安全、依赖未定义的 runtime 行为、可移植性和未来兼容无保证。
- 非用不可时:缩小范围、写清假设、补足测试和文档,并优先考虑交给标准库或少量底层库维护。
- 业务代码:能不用就不用;性能问题先测再下结论,多数场景下安全写法足够。
@startmindmap
<style>
mindmapDiagram {
node {
BackgroundColor #F5F5F5
RoundCorner 8
Padding 8
FontSize 14
}
:depth(0) {
BackgroundColor #2C3E50
FontColor white
FontSize 16
FontStyle bold
}
:depth(1) {
FontSize 14
FontStyle bold
}
:depth(2) {
FontSize 13
}
}
</style>
* 为什么要少用 Go unsafe
** 是什么
*** unsafe.Pointer / Sizeof / Offsetof
*** 绕过类型系统,做底层内存操作
** 为什么保留
*** C 互操作(CGO)
*** 标准库与 runtime 实现
*** 极少数性能关键路径
**[#FFEBEE] 少用的理由
*** 破坏内存安全,bug 难查
*** 违背 GC/栈假设,未定义行为
*** 可移植性/未来兼容无保证
**[#E8F5E9] 何时可考虑用
*** 无安全替代、范围极小
*** 文档与测试拉满
*** 优先集中在底层库
** 类比
*** 类型系统 = 救生员
*** unsafe = 野泳,要有预案与边界
@endmindmap

明天就能做的 3 件事
- 搜一遍自己负责的 Go 仓库里的
unsafe(10 分钟) - 怎么判断做得好:能列出每一处用途、调用链,以及“为什么当时用了 unsafe”。
- 给已有的 unsafe 用法补一句注释(5 分钟/处)
- 写明:依赖什么假设、在什么条件下安全、将来打算如何替代或收敛。
- 对“为了性能想用 unsafe”的地方先做 benchmark(15 分钟)
- 怎么判断:有数据证明安全写法确实是瓶颈,再考虑是否值得引入 unsafe;多数情况会发现不必用。
什么时候该用、什么时候别用
适合“考虑用”的场景:必须和 C 互操作、实现标准库级别的抽象、且范围可控、文档测试都到位。
不适合用的场景:业务逻辑里图方便、想“少一次拷贝”、没有证据证明是性能瓶颈、或者团队里没人能说清当前写法的前提和风险。
代价与权衡:用 unsafe 会提高维护成本和事故概率;只在收益明确、且无法用安全方式实现时,才接受这份代价。
最后一个问题留给你:
你们项目里有没有“历史遗留”的 unsafe 用法?如果有,你打算先做一次梳理和注释,还是等出一次线上问题再动刀?
扩展阅读
- Go 官方文档: unsafe 包
- Go 官方博客: The Laws of Reflection(理解 reflect 与底层表示的边界)
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。