Go运行时仅重定位栈上可追踪指针,即含类型信息且被GC扫描到的指针(如&x、闭包捕获变量、defer参数中的指针);unsafe.Pointer和uintptr不被识别,扩容后仍指向旧地址导致崩溃。

栈扩容时哪些指针会被 runtime 自动重定位
只有被 Go 运行时 GC 扫描器识别为「栈上可追踪指针」的变量,才会在 runtime.morestack → runtime.newstack 过程中被重写地址。这取决于指针是否保留了类型信息和栈帧上下文:
-
&x(x 是普通局部变量)会被重定位:编译器生成的栈帧包含变量布局元数据,GC 能遍历并修正该指针值 - 闭包捕获的变量(如
func() { println(x) }中的x)若未逃逸,仍留在栈上,其地址也会被重映射 -
defer记录的函数参数若含指针(如defer fmt.Println(&x)),参数副本中的指针值会被更新,指向新栈上的x -
unsafe.Pointer或uintptr不会被识别:例如uintptr(unsafe.Pointer(&x))或unsafe.Add(unsafe.Pointer(&x), 4),扩容后这些整数仍指向旧栈地址,读写将 panic 或静默损坏
为什么 unsafe 操作在栈扩容后失效
Go 运行时不做「内存地址模糊匹配」,它只按栈帧结构体偏移 + 类型信息批量重写指针。而 unsafe 把指针转成无类型的整数,切断了与变量生命周期和位置的语义关联:
-
uintptr是纯数值,GC 不扫描它;即使你把它存进map[uintptr]int,运行时也完全不知道它曾代表某个栈变量地址 - 跨 goroutine 传递
uintptr(unsafe.Pointer(&x))是危险的:接收方 goroutine 的栈可能早已复制多次,该地址早已释放 - 典型错误:
go func() { println(uintptr(unsafe.Pointer(&x))) }()—— 这个打印出的数字在扩容后就是悬空地址
defer 和 _defer 结构体如何保持有效性
很多人担心 defer 链在栈搬家后断开,其实不会。因为 _defer 是运行时管理的结构体,本身存储在栈上,但它的字段(如 fn、argp、pc)在 runtime.newstack 中被显式重定位:
-
_defer.fn是函数指针,重定位目标代码段,不受栈复制影响 -
_defer.argp指向 defer 实际参数所在栈位置,运行时会根据旧栈到新栈的偏移量整体加偏移修正 - 即使 defer 内部再调用一个大栈函数(触发二次扩容),当前
_defer结构仍有效,只是执行延迟增加 - 但若 defer 体内用了
unsafe计算的地址(如defer func() { *(*int)(unsafe.Pointer(uintptr(&x) + 8)) = 1 }),就无法保障
容易被忽略的边界情况:CGO 和栈指针泄漏
CGO 是栈指针重定位机制的盲区——C 函数使用系统线程栈,Go 运行时不参与其内存管理,也不会重写传入 C 的任何指针:
- 从 Go 传
&x给 C 函数,C 返回前必须用完,不能长期持有;一旦 Go 栈扩容,C 拿着的仍是旧地址 - C 回调 Go 函数时,若回调中又取了 Go 局部变量地址(比如
void cb() { go_func(&local_var); }),这个&local_var不受 Go 栈保护 - 全局 map / channel 存储栈变量地址(如
m[0] = &x)是隐式泄漏:x 所在栈帧可能被复制多次,map 里存的始终是第一次的地址
真正安全的做法,是让变量逃逸到堆(new(T) 或切片底层数组),或明确用 runtime.KeepAlive 延长栈变量生命周期——但后者不解决地址重定位问题,只防 GC 提前回收。


















