Skip to content
koopa0.dev
← the work

Go Escape Analysis:你的變數住在 Stack 還是 Heap?

讀完這篇,你會理解 Go 編譯器如何決定變數的分配位置,以及如何用 -gcflags=-m 看穿這些決策

article· Jun 26, 2026· 3 min

讀完這篇,你會理解 Go 編譯器如何決定變數的分配位置,以及如何用 -gcflags=-m 看穿這些決策


為什麼該在意這件事

寫 Go 的時候,不會需要手動 malloc / free,但這不代表記憶體分配對你來說無關痛癢

一個回傳 *User 的函數和回傳 User 的函數,效能差異可能超乎你的想像。真正的原因是前者可能觸發 Heap 分配和 GC 掃描,不是「指標比較快」這個常見迷思

決定這一切的,就是 Escape Analysis(逃逸分析)


核心概念:Stack vs Heap

Go 編譯器在編譯期就決定每個變數要分配在哪裡:

分配位置 條件 特性
Stack 變數生命週期不超過函數 快速分配、自動回收、cache 友好
Heap 變數可能在函數結束後被引用 需要 GC 管理、有額外開銷

重點:「逃逸」不是執行期的搬移。編譯器在編譯時就決定了:這個變數一開始就要分配在 Heap 上


四種常見逃逸情境

1. 回傳指標

// user 逃逸到 Heap
func createUserB() *User {
    user := User{Name: "Bob", Age: 25}
    return &user  // 呼叫者需要這個位址 → 必須逃逸
}

// user 留在 Stack
func createUserA() User {
    user := User{Name: "Alice", Age: 30}
    return user  // 值複製 → 原始 user 隨 frame 消失也沒關係
}

2. 閉包捕捉

func counter() func() int {
    count := 0
    return func() int {
        count++      // 閉包捕捉了 count
        return count // counter() 結束後閉包仍需要 count → 逃逸
    }
}

3. 傳給 interface{}

func example() {
    x := 42
    fmt.Println(x) // fmt.Println 接收 any,且無法內聯 → x 逃逸
}

4. 回傳 Slice 底層陣列

func makeSlice() []int {
    s := make([]int, 0, 10)
    s = append(s, 1, 2, 3)
    return s // slice header 值複製,但底層陣列必須逃逸
}

親眼看見逃逸

-gcflags=-m 讓編譯器告訴你決策:

go build -gcflags="-m" main.go

輸出範例:

./main.go:17:9: &user escapes to heap
./main.go:16:2: moved to heap: user

加兩個 -m 看更多細節:

go build -gcflags="-m -m" main.go

三個常見誤解

大型結構一定會逃逸 — 逃逸取決於生命週期和引用關係,不是大小。1MB 的結構如果只在函數內使用且不回傳指標,它仍然可以留在 Stack

用指標一定比較快 — 對於小型結構,值複製的成本可能低於逃逸到 Heap 的分配成本 + GC 掃描成本 + 指標間接存取的 cache miss 成本

逃逸是執行時發生的 — 編譯期決策,執行期只是按照決定分配


什麼時候該管、什麼時候別管

適合優化:

  • Hot path 上的頻繁分配
  • Profiling 顯示 GC 壓力大
  • -gcflags=-m 顯示意外的逃逸

不該過度優化:

  • 沒有效能問題的程式碼
  • 可讀性會嚴重下降
  • 一次性執行的初始化程式碼

快速檢查清單

當你想知道某個變數是否會逃逸:

  • 它的指標有沒有被回傳?
  • 它有沒有被閉包捕捉?
  • 它有沒有被傳給 anyinterface{})?
  • 它有沒有被傳給無法內聯的函數?
  • 它有沒有被存入會被回傳的容器?

如果以上都是「否」,它很可能留在 Stack。有任何一個是「是」,用 -gcflags=-m 確認


總結

  1. Escape Analysis 是編譯期決策,決定變數住 Stack 還是 Heap
  2. 回傳指標、閉包捕捉、interface{}/any 是最常見的逃逸原因
  3. 先用 -gcflags=-m 看見事實,再用 profiling 決定要不要優化