参考资料: go局部变量的存储空间是堆还是栈: https://studygolang.com/articles/11878 Go的变量到底在堆还是栈中分配: https://studygolang.com/articles/7559 go变量逃逸分析: https://www.cnblogs.com/itbsl/p/10476674.html
Go逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用
,那么它就会发生逃逸。
简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。
Go语言里没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量分配到何处。变量是在堆还是栈上分配空间并不是由用var
还是new
声明变量的方式决定的。
例
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
f函数里的x变量在堆上分配,因为它在函数退出后依然可以通过global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量y将是不可达的,也就是说可以马上被回收的。因此,y并没有从函数g中逃逸,编译器可以选择在栈上分配*y的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。
逃逸分析实例
Go提供了相关的命令,可以查看变量是否发生逃逸。
还是用上面我们提到的例子:
package main
import "fmt"
func foo() *int {
t := 3
return &t;
}
func main() {
x := foo()
fmt.Println(*x)
}
foo函数返回一个局部变量的指针,main函数里变量x接收它。执行如下命令:
go build -gcflags '-m -l' main.go
加-l是为了不让foo函数被内联。得到如下输出:
# command-line-arguments
src/main.go:7:9: &t escapes to heap
src/main.go:6:7: moved to heap: t
src/main.go:12:14: *x escapes to heap
src/main.go:12:13: main ... argument does not escape
foo函数里的变量t逃逸了,和我们预想的一致。让我们不解的是为什么main函数里的x也逃逸了?这是因为有些函数参数为interface类型,比如fmt.Println(a ...interface),编译期间很难确定其参数的具体类型,也会发生逃逸。
使用反汇编命令也可以看出变量是否发生逃逸。
go tool compile -S main.go
截取部分结果,图中标记出来的说明t是在堆上分配内存,发生了逃逸。