Golang中的局部變量「什麼時候棧?什麼時候堆?」

1、C/C++報錯?Golang經過?

咱們先看一段代碼git

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

居然沒有報錯!github

瞭解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);
}

編譯bash

$ gcc pro_1.c 
pro_1.c: In function ‘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 
0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720
0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720
0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720
0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720
0xc000030758 0xc000030738 0xc000030730 0xc000082000 0xc000030728 0xc000030720
13 0xc000082000

咱們能看到foo_val3是返回給main的局部變量, 其中他的地址應該是0xc000082000,很明顯與其餘的foo_val一、二、三、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關鍵字

...
 16     0x0021 00033 (pro_2.go:5)   PCDATA  $0, $0
 17     0x0021 00033 (pro_2.go:5)   PCDATA  $1, $0
 18     0x0021 00033 (pro_2.go:5)   MOVQ    $11, "".foo_val1+48(SP)
 19     0x002a 00042 (pro_2.go:6)   MOVQ    $12, "".foo_val2+40(SP)
 20     0x0033 00051 (pro_2.go:7)   PCDATA  $0, $1
 21     0x0033 00051 (pro_2.go:7)   LEAQ    type.int(SB), AX
 22     0x003a 00058 (pro_2.go:7)   PCDATA  $0, $0
 23     0x003a 00058 (pro_2.go:7)   MOVQ    AX, (SP)
 24     0x003e 00062 (pro_2.go:7)   CALL    runtime.newobject(SB)  //foo_val3是被new出來的
 25     0x0043 00067 (pro_2.go:7)   PCDATA  $0, $1
 26     0x0043 00067 (pro_2.go:7)   MOVQ    8(SP), AX
 27     0x0048 00072 (pro_2.go:7)   PCDATA  $1, $1
 28     0x0048 00072 (pro_2.go:7)   MOVQ    AX, "".&foo_val3+56(SP)
 29     0x004d 00077 (pro_2.go:7)   MOVQ    $13, (AX)
 30     0x0054 00084 (pro_2.go:8)   MOVQ    $14, "".foo_val4+32(SP)
 31     0x005d 00093 (pro_2.go:9)   MOVQ    $15, "".foo_val5+24(SP)
 32     0x0066 00102 (pro_2.go:9)   XORL    CX, CX
 33     0x0068 00104 (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 
666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730
666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730
666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730
666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730
666 0xc000030728 0xc000030720 0xc00001a0e0 0xc000030738 0xc000030730
0 0xc00001a0e0

很明顯, foo_val3的地址0xc00001a0e0 依然與其餘的不是連續的. 依然具有逃逸行爲.

4、結論

Golang中一個函數內局部變量,不論是不是動態new出來的,它會被分配在堆仍是棧,是由編譯器作逃逸分析以後作出的決定。

按理來講, 人家go的設計者明明就不但願開發者管這些,可是面試官就恰恰找這種問題問? 醉了也是.



文章推薦

開源軟件做品

(原創開源)Zinx-基於Golang輕量級服務器併發框架-完整版(附教程視頻)
(原創開源)Lars-基於C++負載均衡遠程調度系統-完整版

精選文章

典藏版-Golang調度器GMP原理與調度全分析
使用Golang的interface接口設計原則
深刻淺出Golang的協程池設計
Go語言構建微服務一站式解決方案


關於做者:

做者:Aceld(劉丹冰)

mail: danbing.at@gmail.com
github: https://github.com/aceld
原創書籍gitbook: http://legacy.gitbook.com/@aceld

關注做者

微信公衆號「劉丹冰Aceld」

相關文章
相關標籤/搜索