go 的写屏障是并发标记阶段正确性的强制保障,用于防止三色标记法中的漏标问题;它由编译器自动插入,仅在标记阶段生效,拦截堆到堆及栈到堆的指针写操作,确保gc不误回收对象。

Go 的写屏障(Write Barrier)不是可选优化,而是并发标记阶段正确性的强制保障——没有它,GC 会漏标、误回收,程序直接崩溃。
为什么 Go 必须用写屏障?
三色标记法本身在并发场景下是脆弱的:用户 goroutine 正在运行,同时 GC 在后台标记对象,如果此时某条赋值语句把一个白色对象挂到黑色对象下(a.b = whiteObj),而该黑色对象已结束扫描,这个白色对象就永远逃过标记,被错误清除。
这就是经典的「黑色对象引用白色对象」漏标问题。Go 不靠 STW 来规避它,而是靠写屏障实时拦截指针写操作:
- 混合写屏障(Hybrid Barrier,Go 1.8+ 默认)在写指针前,把原指针指向的对象标记为灰色(
shade),并把新指针也记入待扫描队列 - 栈上对象不插屏障,但会在标记终止阶段 STW 扫描所有 goroutine 栈,确保根可达性完整
- 写屏障开销极小(几条原子指令),但换来的是微秒级 STW 和正确性保证
runtime.gcWriteBarrier 是什么?
这不是你手动调用的函数,而是编译器在生成赋值指令时自动插入的底层汇编桩(stub)。你写的每一行 obj.field = otherObj,只要 obj 和 otherObj 都在堆上,编译器就会在中间插入 runtime.gcWriteBarrier 调用。
它不暴露给 Go 代码,也不该被绕过——试图用 unsafe 或内联汇编跳过它,会导致 GC 状态错乱,表现为随机 panic、内存踩踏或静默数据损坏。
验证方式:用 go tool compile -S main.go 查看汇编输出,搜索 gcWriteBarrier 即可见其插入位置。
写屏障开启/关闭会影响哪些行为?
写屏障由 GC 状态控制,只在标记阶段(gcMarkDone 之前)启用。它对以下行为有直接影响:
-
new/make分配的对象:不受影响,它们天然是白色,由标记阶段决定是否存活 - 栈上变量赋值(如
localPtr = heapObj):不触发写屏障,但会在标记终止 STW 时被栈扫描捕获 - map/slice 的底层扩容或 rehash:内部指针更新会走写屏障,所以 map 写操作安全
- CGO 边界(如
C.malloc返回的指针被存入 Go 结构体字段):若未通过runtime.Pinner或unsafe.Pointer显式管理,可能逃逸写屏障检查,导致悬空引用
容易被忽略的边界情况
写屏障只保护「堆到堆」和「栈到堆」的指针写入,但它不管:
- 直接操作内存地址(
(*int)(unsafe.Pointer(uintptr(ptr)+offset)) = 42)——完全绕过类型系统和写屏障 - 通过
reflect.Value.SetPointer修改指针字段——反射会触发写屏障,但reflect.Value.UnsafeAddr+ 原生写则不会 - 全局变量初始化阶段(init 函数中)的指针赋值——此时 GC 尚未启动,写屏障未生效,但这些对象天然是根对象,不会被漏标
真正危险的是:你以为自己在「安全地」做指针重定向,实际却在 GC 标记窗口期破坏了对象图拓扑——这类 bug 往往只在高负载、长时间运行后才暴露,且难以复现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











