# 零分配的秘密：Context pool 與 smartParams

> gortex 路由零分配的三個手法：sync.Pool 重用 Context、[4]string 的 smartParams、內嵌 responseWriter，再用 benchmem 驗證。

原文：https://sheng.page/posts/gortex-zero-allocation-context/ · 發布：2026-04-05 · 作者：Sheng

gortex 的路由 hot path 是 0 allocs/op，秘密是三件事：`sync.Pool` 重用 Context、`[4]string` 的 smartParams、內嵌的 responseWriter。這篇逐一拆，並教你怎麼量。

> 前置閱讀：〈[記憶體與併發原語](/posts/go-memory-and-sync-primitives/)〉

## sync.Pool 重用 Context

每條請求都要一個 Context，每次 `new` 一個就是每秒幾萬次 heap 配置加 GC 壓力。gortex 改成從 `sync.Pool` 借：

```go
var contextPool = sync.Pool{
    New: func() any { return &DefaultContext{store: make(Map)} }, // 預配 store map
}

func AcquireContext(r *http.Request, w http.ResponseWriter) Context {
    c := contextPool.Get().(*DefaultContext)
    c.Reset(r, w) // 綁新的 request/response
    return c
}
```

關鍵在歸還。`ReleaseContext` 把欄位清空再放回，但**保留已配置的記憶體**：store 用 `for k := range m { delete(m, k) }` 逐一刪 key、不重 `make`（留住底層 bucket）；params 呼叫 `reset()` 而不是設 nil（留住 backing array）。下一條請求拿到的就是一個俐落、但記憶體已就緒的 Context，這就是記憶體那篇講的 `sync.Pool` 重用落到實戰。

## smartParams：[4]string 的 SBO

路徑參數（`:id` 那些）若每次都開一個 map，又是一次配置。gortex 用 small-buffer optimization：

```go
type smartParams struct {
    count    int
    keys     [4]string   // 常見情況（≤4 個參數）全放固定陣列，不進 heap
    vals     [4]string
    overflow []pathParam // 超過 4 個才退化到 slice
}
```

`[4]string` 是值陣列，內嵌在 Context 裡，不額外配置。選 4 是因為多數 REST 路徑參數不超過三四層。超出才用 `overflow` slice。

這裡特意用 **insertion-ordered slice 而不是 map**：〈[Segment-trie 路由器：不靠正則的匹配](/posts/gortex-segment-trie-router/)〉比對時 Static > Param > Wildcard 會回溯，失敗的 param 分支要能精準退掉它加過的參數。`truncate(n)` 就靠順序把 index ≥ n 的丟掉：

```go
func (sp *smartParams) truncate(n int) {
    sp.count = n
    switch {
    case n <= 4:
        sp.overflow = sp.overflow[:0]
    case n-4 < len(sp.overflow):
        sp.overflow = sp.overflow[:n-4]
    }
}
```

## 內嵌 responseWriter

要記錄回應狀態碼與大小（給 logger、給「是否已寫入」的判斷），得包一層 `http.ResponseWriter`。gortex 的包法用到〈[interface 與組合](/posts/go-interfaces-and-composition/)〉的 embedding：

```go
type responseWriter struct {
    http.ResponseWriter // 內嵌：原本的方法直接繼承，不花成本
    status  int
    size    int64
    written bool
}
```

內嵌 `http.ResponseWriter` 後，這個 wrapper 自動滿足介面，只要覆寫 `WriteHeader`/`Write` 去記 status 與 size。更關鍵的是它在 `DefaultContext` 裡是 **value 欄位**（`rw responseWriter`），跟 pooled context 同生命週期，Reset 時重新指向新的 `http.ResponseWriter`，免去每條請求 new 一個 wrapper 的成本。狀態追蹤因此零額外配置。

## 怎麼量 allocs/op

零分配不能用講的，要量。`go test -benchmem` 會吐 `allocs/op`：

```go
func BenchmarkRoute(b *testing.B) {
    r := setupRouter()
    req := httptest.NewRequest("GET", "/users/42", nil)
    w := httptest.NewRecorder()
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        r.ServeHTTP(w, req)
    }
}
// go test -bench=BenchmarkRoute -benchmem
// → ... ns/op    0 B/op    0 allocs/op
```

依 gortex README 的 benchmark，路由 hot path 約 65 ns/op、0 allocs/op（M3 Pro）。只要不小心讓一個值逃逸（塞進 interface、寫一次 map），`allocs/op` 立刻不是 0，所以是量出來的，不是猜的。


## 補充筆記

- Context 從 `sync.Pool` 借用歸還；清理時保留 map 與 params 的 backing，下一條請求 0 alloc。
- smartParams 用 `[4]string` SBO，≤4 個參數不進 heap；overflow 用 insertion-ordered slice，配合 segment-trie 那篇的回溯精準 `truncate`。
- `responseWriter` 內嵌 `http.ResponseWriter`（embedding 省去重寫方法）且是 pooled context 的 value 欄位，狀態追蹤零額外配置。
- 用 `go test -benchmem` 看 `allocs/op`；gortex 路由約 65 ns/op、0 allocs/op（README，M3 Pro）。
- 零分配就是記憶體與併發原語那篇（逃逸分析 / pool / 同步原語）落到框架 hot path 的成果。

原始碼：[yshengliao/gortex](https://github.com/yshengliao/gortex)。

---

> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 環境 Go 1.24（gortex go.mod）· 本系列為事後回填整理 · 列入 20260613 blog 翻新計劃，新漆未乾。