Golang中的栈分配与堆分配对比
Golang中的栈分配与堆分配对比 我是Go语言的新手,有C/C++背景。我在理解变量是分配在栈上还是堆上时遇到了困难。
func main() {
var i1 = 10
var i2 *int = new(int)
fmt.Println(&i1, i2)
}
在上面的代码中,i1应该分配在栈上,而i2应该分配在堆上。然而,运行上述代码得到的结果是:
0xc0000a0000 0xc0000a0008
这证明i1和i2在内存中位置很接近(具体相隔8个字节)。i2必须分配在堆上,这意味着i1也分配在堆上。为什么i1没有放在栈上?
首先,你可以使用 go build -gcflags '-m' 来检查逃逸分析的结果。不过据我回忆,传递给 print 类函数(如 Println)的任何参数都会逃逸到堆上,因为这些函数接受的 interface 类型参数需要进行动态分发,从而导致逃逸。在你的示例中,如果 i1 和 i2 没有被用作参数(参见第1点),它们本不应逃逸,但实际情况是规范并没有明确规定变量何时逃逸。
话虽如此,我知道他们一直在改进逃逸分析,所以这些信息可能已经过时。请不要引用我的说法,我也很希望有人能在此纠正我。
1 - 需要澄清的是,我写的内容取决于语言和内存分配的哲学。在 Go 中,new() 在某些情况下被保证会逃逸,因为当时只有一个实现且它确实逃逸了,但大多数这类问题实际上与具体实现相关。
func main() {
fmt.Println("hello world")
}
在Go语言中,变量是分配在栈上还是堆上是由编译器的逃逸分析(escape analysis)决定的,而不是像C/C++那样由程序员显式控制。逃逸分析会判断变量的生命周期是否超出了当前函数的范围,如果超出,则变量会被分配到堆上,否则分配到栈上。
在你的代码示例中,i1 是一个整型变量,而 i2 是一个指向整型的指针。虽然 i1 看起来是一个局部变量,但由于你使用了 &i1 获取了它的地址,并且传递给了 fmt.Println 函数,这导致 i1 的引用逃逸到了函数外部。因此,编译器会将其分配到堆上,而不是栈上。这就是为什么 i1 和 i2 的地址在内存中位置相近的原因——它们都被分配在堆上。
下面是一个简单的示例,展示如何通过逃逸分析来确认变量的分配位置:
package main
import "fmt"
// 使用go build -gcflags="-m"来查看逃逸分析结果
func main() {
var i1 = 10
var i2 *int = new(int)
fmt.Println(&i1, i2)
}
运行 go build -gcflags="-m" 命令,你会看到类似以下的输出:
./main.go:8:13: ... argument does not escape
./main.go:8:14: &i1 escapes to heap
./main.go:6:6: moved to heap: i1
./main.go:7:17: new(int) escapes to heap
从输出中可以看到,i1 和 new(int) 都“逃逸到了堆”(escapes to heap),因此它们被分配在堆上。
如果你希望 i1 分配在栈上,可以避免获取其地址或将其传递给可能使引用逃逸的函数。例如:
package main
import "fmt"
func main() {
var i1 = 10
var i2 *int = new(int)
// 不获取i1的地址,只使用值
fmt.Println(i1, i2)
}
运行 go build -gcflags="-m" 后,输出可能显示 i1 没有逃逸:
./main.go:8:13: ... argument does not escape
./main.go:7:17: new(int) escapes to heap
总结:在Go中,变量的栈/堆分配由编译器自动管理,基于逃逸分析。获取变量地址并传递到函数外部通常会导致变量逃逸到堆。


