記憶體與併發原語

零分配不是玄學,是逃逸分析加物件重用加選對同步原語。這篇把 sync.Poolatomic、Mutex/RWMutex 擺一起講,之後看 gortex 的零分配 hot path 才不會卡。

逃逸分析:stack 還是 heap

變數放 stack 還是 heap,由編譯器在編譯期決定,不是看你寫 new 還是 &。能證明變數不會活過函式作用域就放 stack(不歸 GC 管,函式返回就回收);一旦被外部引用,像回傳指標、塞進 interface、被 closure 捕捉、存進會逃逸的結構,就 escape 到 heap。

func New() *T { return &T{} } // &T 逃逸到 heap:指標被回傳出去了

go build -gcflags='-m' 可以看每個變數的逃逸決策。零分配的第一步,就是砍掉不必要的 escape。

sync.Pool:重用降 GC 壓力

sync.Pool 重用短命物件,把配置成本與 GC 壓力分散掉。Get 拿一個(池子空了就呼叫 New),用完 Put 還回去:

var bufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}

b := bufPool.Get().(*bytes.Buffer)
b.Reset()            // 放回前/拿出後要清空
defer bufPool.Put(b)

兩個重點:還回去前要 reset,避免髒資料外洩;而且不能假設池子一定留著你的物件,GC 會清掉 Pool。gortex 每條請求的 Context 就是從這種 pool 拿、用完歸還(B4〈零分配的秘密〉,gortex-zero-allocation-context)。

atomic vs Mutex vs RWMutex

  • atomic:對單一整數或指標做無鎖操作(AddInt64CompareAndSwap),最快,但只保護得了那一個值。
  • sync.Mutex:保護一段臨界區,適合一次要改好幾個欄位。
  • sync.RWMutex:讀多寫少時,多個讀並行、寫獨佔;但它比 Mutex 重,讀寫比不夠懸殊反而更慢。

選法一句話:單一計數用 atomic,一塊狀態用 Mutex,讀遠多於寫才考慮 RWMutex。gortex 的 sharded metrics 同時用上 atomic 計數與 per-shard 鎖(B5〈sharded metrics 與 circuit breaker〉)。

Effective Go 精選:Data

Effective Go 的 Data 講配置的基本盤:new(T) 配一塊歸零記憶體、回 *Tmake 專給 slice / map / channel 並做初始化;composite literal &T{...} 直接給值。搞懂這幾個配置路徑,才分得出哪些值落在 heap、哪些能留在 stack 省掉。

重點整理

  • 逃逸分析決定 stack/heap,-gcflags='-m' 看;回傳指標、進 interface 容易逃逸。
  • sync.Pool 重用物件攤平 GC;放回前 reset,別假設物件一定還在。
  • 選同步原語:atomic(單值)→ Mutex(一段狀態)→ RWMutex(讀多寫少才划算)。
  • 這三樣是 gortex 零分配 hot path 的前置:B4 的 Context pool、B5 的 sharded metrics 都建在這上面。

延伸:本系列接著用這些觀念拆 gortex 框架,原始碼見 yshengliao/gortex


大綱 Sheng,內文 Claude 協助 · 環境 Go 1.25(當時最新 release)· 本系列為事後回填整理 · 列入 20260613 blog 翻新計劃,新漆未乾。