在 C语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。而Go语言将这个过程整合到了编译器中,命名为“变量逃逸分析”。通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配。
package main
import (
"fmt"
)
type Data struct {
name string
id int
}
var gpi *int
var gf float64
func putf() *Data {
var c =Data{"hello",2}
i:=3
f:=3.14159
//这里是把局部变量赋值给外部变量,函数体结束完,不需使用该变量,所以在栈上分配内存的
gf=f
//把函数局部变量地址赋值给外部全局指针变量gpi,在函数退出时,还需要使用该变量,所以编译器会自动在堆上分配内存。
gpi=&i
//返回函数局部变量地址,跟上边一样是在堆上分配内存。
return &c
}
func puti() int {
//这里直接返回局部变量的值,变量是在栈内分配内存即可,函数退出时该变量将不再使用。
t:=4
return t
}
func main() {
var ret=putf()
fmt.Printf("ret:%T-%v-%v\n",ret,ret,*ret)
fmt.Printf("gpi:%T-%v-%v\n",gpi,gpi,*gpi)
fmt.Printf("gf:%T-%v\n",gf,gf)
ri:=puti()
fmt.Printf("ri:%T-%v\n",ri,ri)
}
使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,运行结果如下:
在这段代码中,putf函数里声明了有几个局部变量,有返回这些局部变量的地址,也有直接赋值给外部变量的。在c语言中,如返回一个局部变量的地址,这样写法是肯定错误的,因为函数里的局部变量是存放在栈中,当函数结束时,栈内存所有数据会自动清空结束,将导致获取到的地址是一个非法地址空间。而go语言编译器会判断检测到这一错误,将要返回局部变量地址的转移到堆上,就是"moved to heap:c,moved to heap:i"这样信息,然后由垃圾回收器去回收这些变量的内存。而局部变量f是直接赋值给外部变量,在函数体结束后就不需使用该局部变量,所以就直接在栈上分配该变量,看不到在堆上分配的信息。
最后 var ret=putf() fmt.Printf("%T-%v-%v",ret,ret,*ret) 就是打印出返回值的类型,用格式符%T,*main.Data就是表示是一个Data类型的指针。打印出具体数值,用格式符%v,可以看到是&{hello 2}就是它是一个指向{hello 2}结构体的地址。因为ret是一个结构体指针类型,所以*ret就是获取的结构体内容,打印的是{hello 2}
总结,在使用Go语言进行编程时,Go语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆的问题上,编译器会自动帮助开发者完成这个纠结的选择,并通过垃圾回收机制自动去回收分配的内存。