記憶體與併發原語
記憶體與併發原語
零分配不是玄學,是逃逸分析加物件重用加選對同步原語。這篇把 sync.Pool、atomic、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:對單一整數或指標做無鎖操作(AddInt64、CompareAndSwap),最快,但只保護得了那一個值。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) 配一塊歸零記憶體、回 *T;make 專給 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 翻新計劃,新漆未乾。