最灵繁的人也看不见自己的背脊。——非洲
1 C/C++报错?Golang通过?
我们先看一段代码
package main
func foo(arg_val int)(*int){
var foo_val int=11;
return&foo_val;
}
func main(){
main_val := foo(666)
println(*main_val)
}
编译运行
$ go run pro_1.go
11
竟然没有报错!
了解C/C++的小伙伴应该知道,这种情况是一定不允许的,因为 外部函数使用了子函数的局部变量, 理论来说,子函数的foo_val 的声明周期早就销毁了才对,如下面的C/C++代码
#include<stdio.h>
int*foo(int arg_val){
int foo_val =11;
return&foo_val;
}
int main()
{
int*main_val = foo(666);
printf("%d\n",*main_val);
}
编译
$ gcc pro_1.c
pro_1.c:Infunction‘foo’:
pro_1.c:7:12: warning:function returns address of local variable [-Wreturn-local-addr]
return&foo_val;
^~~~~~~~
出了一个警告,不管他,再运行
$ ./a.out
段错误(核心已转储)
程序崩溃.
如上C/C++编译器明确给出了警告,foo把一个局部变量的地址返回了;反而高大上的go没有给出任何警告,难道是go编译器识别不出这个问题吗?
2 Golang编译器得逃逸分析
go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做 逃逸分析(escape analysis),当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆 。go语言声称这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。
我们再看如下代码:
package main
func foo(arg_val int)(*int){
var foo_val1 int=11;
var foo_val2 int=12;
var foo_val3 int=13;
var foo_val4 int=14;
var foo_val5 int=15;
//此处循环是防止go编译器将foo优化成inline(内联函数)
//如果是内联函数,main调用foo将是原地展开,所以foo_val1-5相当于main作用域的变量
//即使foo_val3发生逃逸,地址与其他也是连续的
for i :=0; i <5; i++{
println(&arg_val,&foo_val1,&foo_val2,&foo_val3,&foo_val4,&foo_val5)
}
//返回foo_val3给main函数
return&foo_val3;
}
func main(){
main_val := foo(666)
println(*main_val, main_val)
}
我们运行一下
$ go run pro_2.go
0xc0000307580xc0000307380xc0000307300xc0000820000xc0000307280xc000030720
0xc0000307580xc0000307380xc0000307300xc0000820000xc0000307280xc000030720
0xc0000307580xc0000307380xc0000307300xc0000820000xc0000307280xc000030720
0xc0000307580xc0000307380xc0000307300xc0000820000xc0000307280xc000030720
0xc0000307580xc0000307380xc0000307300xc0000820000xc0000307280xc000030720
130xc000082000
我们能看到foo_val3是返回给main的局部变量, 其中他的地址应该是0xc000082000,很明显与其他的foo_val1、2、3、4不是连续的.
我们用go tool compile测试一下
$ go tool compile -m pro_2.go
pro_2.go:24:6: can inline main
pro_2.go:7:9: moved to heap: foo_val3
果然,在编译的时候, foo_val3具有被编译器判定为逃逸变量, 将foo_val3放在堆中开辟.
我们在用汇编证实一下:
$ go tool compile -S pro\_2.go > pro\_2.S
打开pro_2.S文件, 搜索runtime.newobject关键字
...
160x002100033(pro_2.go:5) PCDATA $0, $0
170x002100033(pro_2.go:5) PCDATA $1, $0
180x002100033(pro_2.go:5) MOVQ $11,"".foo_val1+48(SP)
190x002a00042(pro_2.go:6) MOVQ $12,"".foo_val2+40(SP)
200x003300051(pro_2.go:7) PCDATA $0, $1
210x003300051(pro_2.go:7) LEAQ type.int(SB), AX
220x003a00058(pro_2.go:7) PCDATA $0, $0
230x003a00058(pro_2.go:7) MOVQ AX,(SP)
240x003e00062(pro_2.go:7) CALL runtime.newobject(SB)//foo_val3是被new出来的
250x004300067(pro_2.go:7) PCDATA $0, $1
260x004300067(pro_2.go:7) MOVQ 8(SP), AX
270x004800072(pro_2.go:7) PCDATA $1, $1
280x004800072(pro_2.go:7) MOVQ AX,"".&foo_val3+56(SP)
290x004d00077(pro_2.go:7) MOVQ $13,(AX)
300x005400084(pro_2.go:8) MOVQ $14,"".foo_val4+32(SP)
310x005d00093(pro_2.go:9) MOVQ $15,"".foo_val5+24(SP)
320x006600102(pro_2.go:9) XORL CX, CX
330x006800104(pro_2.go:15) JMP 252
...
看出来, foo_val3是被runtime.newobject()在堆空间开辟的, 而不是像其他几个是基于地址偏移的开辟的栈空间.
3 new的变量在栈还是堆?
那么对于new出来的变量,是一定在heap中开辟的吗,我们来看看
package main
func foo(arg_val int)(*int){
var foo_val1 *int=new(int);
var foo_val2 *int=new(int);
var foo_val3 *int=new(int);
var foo_val4 *int=new(int);
var foo_val5 *int=new(int);
//此处循环是防止go编译器将foo优化成inline(内联函数)
//如果是内联函数,main调用foo将是原地展开,所以foo_val1-5相当于main作用域的变量
//即使foo_val3发生逃逸,地址与其他也是连续的
for i :=0; i <5; i++{
println(arg_val, foo_val1, foo_val2, foo_val3, foo_val4, foo_val5)
}
//返回foo_val3给main函数
return foo_val3;
}
func main(){
main_val := foo(666)
println(*main_val, main_val)
}
我们将foo_val1-5全部用new的方式来开辟, 编译运行看结果
$ go run pro_3.go
6660xc0000307280xc0000307200xc00001a0e00xc0000307380xc000030730
6660xc0000307280xc0000307200xc00001a0e00xc0000307380xc000030730
6660xc0000307280xc0000307200xc00001a0e00xc0000307380xc000030730
6660xc0000307280xc0000307200xc00001a0e00xc0000307380xc000030730
6660xc0000307280xc0000307200xc00001a0e00xc0000307380xc000030730
00xc00001a0e0
很明显, foo_val3的地址0xc00001a0e0依然与其他的不是连续的. 依然具备逃逸行为.
4 结论
Golang中一个函数内的局部变量,不管是不是动态new出来的,它会被分配在堆还是栈,是由编译器做逃逸分析之后做出的决定。
按理来说, 人家go的设计者明明就不希望开发者管这些,但是面试官就偏偏找这种问题问? 醉了也是。
5 关注公众号
微信公众号: 堆栈future
扫我关注