最灵繁的人也看不见自己的背脊。——非洲
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