Go 的 Stack 與 Heap
從分配機制、生命週期、管理方式以及 Go 的逃逸分析來理解 Stack(棧)與 Heap(堆)的差異。
1. 核心概念與用途
Stack(棧 / 堆棧)
- 用途:函數執行,現代電腦執行模型(基於「堆棧」)的基礎。
- 內容:函數的局部變數、參數、返回值與調用時的上下文資訊。
- 特性:分配與回收極快,隨函數調用與返回自動進行(LIFO,後進先出)。
Heap(堆)
- 用途:存放需跨越函數邊界、或體積較大的資料。
- 內容:動態分配的物件。
- 特性:分配彈性大,管理成本較高;在 Go 中由垃圾回收器 (GC) 回收。
2. 逃逸分析 (Escape Analysis)
Go 與 C/C++ 的重要差異:在 C 中回傳局部變數的位址會造成懸空指標,在 Go 中則可合法使用,靠的是逃逸分析。
- 機制:編譯器分析變數作用域;若函數內的局部變數在函數返回後仍被引用(例如回傳其位址),則判定該變數「逃逸」出棧。
- 結果:逃逸的變數會被自動改放到 Heap,以保證函數返回後仍可使用。
- 範例:
func sum(a, b int) *int {
s := a + b
return &s // s 經逃逸分析後會分配到 Heap
}
3. 分配原語:new 與 make
| 函數 | 功能 | 回傳 | 存放位置 |
|---|---|---|---|
new(T) | 分配並置零 (zeroed) | 指向零值的指標 *T | Stack 或 Heap,依逃逸分析 |
make(T, args) | 建立 slice、map、channel | 已初始化的值 T(非指標) | 依逃逸分析 |
- 為何需要
make:slice、map、channel 底層為引用型別,必須先初始化內部結構(指標、長度、容量等)才能使用,make負責這份初始化。
4. Goroutine 的 Stack 管理
- 動態成長:Goroutine 的 Stack 初始很小(數 KB),隨需要自動擴展。
- 與 Heap 的關係:Stack 空間不足時,執行時會在 Heap 上分配新區塊來擴展 Stack。
總結比較
| 項目 | Stack(棧) | Heap(堆) |
|---|---|---|
| 主要用途 | 函數調用、局部變數、參數傳遞 | 動態分配、跨函數共享、大物件 |
| 生命週期 | 隨函數調用建立,隨返回銷毀 | 由 GC 管理,無引用時回收 |
| 分配速度 | 極快(編譯期/指令) | 較慢(執行期尋找空間) |
| 由誰決定 | 變數未逃逸 | 變數發生逃逸 |
| 常見寫法 | var i int 等一般宣告 | new、make、& 取址(是否上堆仍看逃逸) |
| Goroutine | 初始小、可動態成長 | Stack 成長時會使用 Heap 空間 |
一句話:Go 不需像 C 一樣手動管理 Heap(malloc/free),編譯器透過逃逸分析自動決定變數放在 Stack(求效能)或 Heap(保生存期)。