前情提要

Go 值存储在哪里 一文中提到:

  • 函数的参数和部分返回值会优先尝试通过寄存器传递,而不是全部通过内存(栈)
  • 栈存储局部变量、函数参数和返回值

那么这两句话不矛盾嘛?函数参数和返回值到底存储在了哪里?

这两处描述看似矛盾,但实际上反映了 Go 语言在 演进过程中的优化 以及 “逻辑位置”与“物理位置”的区别

简单来说:它们并不矛盾,而是一种“降级(Fallback)”和“备份”的关系

寄存器是“第一选择”,栈是“保底方案”

在 Go 1.17 之前,Go 采用的是 基于栈(Stack-based) 的调用约定,所有的参数和返回值都老老实实地排队放在栈内存里。

Go 1.17+ 引入 基于寄存器(Register-based) 的调用约定后,情况变成了这样:

  • 优先寄存器:如果参数比较少(例如 9 个以内的整数或指针类型),编译器会直接把它们塞进 CPU 寄存器。
  • 溢出到栈:如果参数太多(超过了寄存器的数量),或者参数类型太复杂(如巨大的结构体),超出的部分仍然会存储在上。

所以,栈依然是存储参数和返回值的“大本营”,只是现在大部分时候它们在寄存器里跑得更快。

物理存储 vs 逻辑备份(Spilling)

即使一个参数是通过寄存器传递的,编译器往往还是会在上为它预留一个空间。这被称为 寄存器溢出(Spilling)

  • 为什么要预留?当发生函数嵌套调用、协程抢占(Preemption)或者垃圾回收(GC)扫描时,CPU 寄存器里的值需要临时找个地方“存一下”,这个地方就是
  • 调试需求:在断点调试或打印堆栈信息(Panic)时,系统需要从栈上恢复出函数当时的参数状态。

局部变量的分配差异

  • 寄存器:通常只存放“当前正在参与运算”的局部变量。
  • :存放那些“虽然是局部变量,但寄存器放不下”或者“需要取地址(&obj)”的变量。只要你对一个局部变量使用了 & 取地址操作(且它没逃逸到堆),它就必须在上有一个明确的内存地址,而不能只在寄存器里。

总结

你可以这样理解两者的关系:

  • 寄存器(高速公路):是函数参数和返回值传递的首选通道,追求极致性能。
  • 栈(停车场):是这些值的物理归宿。它们在不忙的时候、参数太多的时候、或者需要被管理(如 GC/调试)的时候,都会回到栈上。

修改建议: 如果你觉得容易误解,可以把文档描述微调为:

  • 寄存器:存储高频使用的局部变量,以及优先用于函数参数/返回值的传递。
  • :存储无法放入寄存器的局部变量、参数和返回值,以及作为寄存器数据的溢出备份区域