🎯 什麼情境該想到我

當你「需要一個有明確存取順序的暫存區」的時候。
★ 概念簡單,但在 Go 裡「該用什麼型別」是有明確答案的——而且多數人第一次會選錯。

⚙️ 怎麼用(機制與 Go 實作)

兩種順序

規則直覺主要用途
堆疊 StackLIFO 後進先出疊盤子遞迴、回溯、撤銷、括號匹配、DFS
佇列 QueueFIFO 先進先出排隊BFS、工作排程、緩衝、速率限制

★ Go 的堆疊:直接用 slice,不要用 container/list

stack := []int{}
stack = append(stack, v)              // push — 攤還 O(1)
top := stack[len(stack)-1]            // peek
stack = stack[:len(stack)-1]          // pop

為什麼 slice 是對的:

  • append 是攤還 (容量不足時倍增,見下方推導)
  • 連續記憶體 → 快取友善
  • 零額外配置(pop 只是改長度)

⚠️ 持有指標時要手動清零,否則被 pop 掉的元素仍被 slice 的底層陣列引用,GC 收不掉:

stack[len(stack)-1] = nil          // ★ 避免記憶體洩漏
stack = stack[:len(stack)-1]

這是 Go 裡很典型的一個坑——slice 縮短不會清除底層陣列的內容。

★ Go 的佇列:slice 的天真做法有問題

// ❌ 錯誤示範
queue = append(queue, v)
head := queue[0]
queue = queue[1:]        // ★ 看起來 O(1),但底層陣列的頭部永遠不會被回收

問題:queue[1:] 只是移動 slice 的起點,被丟掉的前綴仍然佔著底層陣列。
一個跑很久的佇列會讓底層陣列無限成長(雖然 len 很小),直到 append 觸發重新配置才釋放。

三個正確做法:

做法適用
① 索引 + 定期壓縮用 head 索引而非 reslice,head 超過一半時 copy 壓縮
② ★ 環形緩衝(ring buffer)固定容量、、無配置——高效能場景的標準答案
③ chan★ 需要跨 goroutine 時的正解:有界、執行緒安全、可搭配 select(見 工具-靠通訊共享記憶體)
// ② 環形緩衝
type Ring[T any] struct {
    buf        []T
    head, tail, n int
}
func (r *Ring[T]) Push(v T) bool {
    if r.n == len(r.buf) { return false }   // 滿了
    r.buf[r.tail] = v
    r.tail = (r.tail + 1) % len(r.buf)      // ★ 取模繞回去
    r.n++
    return true
}
func (r *Ring[T]) Pop() (T, bool) {
    var zero T
    if r.n == 0 { return zero, false }
    v := r.buf[r.head]
    r.buf[r.head] = zero                    // ★ 清零,讓 GC 能回收
    r.head = (r.head + 1) % len(r.buf)
    r.n--
    return v, true
}

★ 不要用 container/list(雙向鏈結串列)。
它每個元素都是獨立配置的節點,快取極不友善、GC 壓力大,而且沒有泛型(存 any 要斷言)。
在 Go 裡它幾乎永遠是錯的選擇(見 鏈結串列)。

★ 為什麼 append 是「攤還 」

Go 的 slice 在容量不足時會配置一個更大的陣列並複製。單次可能是 ,但平均是 。

攤還分析(聚合法):容量倍增時,插入 個元素的總複製成本是

(等比級數)→ 平均每次 。

★ 倍增是關鍵。 如果每次只 +1 擴容,總成本會變成 ,
平均每次 。「倍增」把它壓成常數。
同樣的推導也解釋了 雜湊表 的 rehash 為什麼是攤還 。

幾個值得記住的應用

應用用哪個關鍵
DFS堆疊顯式堆疊可避免遞迴爆掉(見 深度優先搜尋)
BFS佇列必須是 FIFO,用堆疊就變成 DFS 了(見 廣度優先搜尋)
括號匹配/表達式求值堆疊經典題
撤銷/重做兩個堆疊undo stack + redo stack
★ 單調堆疊堆疊維持堆疊內單調遞增/遞減, 解決「下一個更大元素」「最大矩形」這類問題——一個很強的技巧
雙端佇列(deque)兩端都能進出滑動視窗最大值的 解法
工作佇列/限流佇列(chan)有界 channel 天然提供背壓

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • ★ Go 的 slice 佇列會洩漏底層陣列(見上)。長壽命佇列一定要用環形緩衝或定期壓縮。
  • ★ 持有指標/介面時 pop 要清零,否則 GC 收不掉。
  • 不要用 container/list。
  • 跨 goroutine 一律用 chan,不要自己加鎖包 slice——chan 已經處理好同步、阻塞與關閉語義。
  • 無界佇列是生產事故的常見來源。生產者比消費者快時會吃光記憶體。用有界 channel 製造背壓(見 工具-降低在製品WIP、工具-MQ消費端防禦三原則)。
  • 堆疊深度有限。遞迴 DFS 在深圖上會爆堆疊(Go 的 goroutine 堆疊會成長但有上限,預設 1 GB)。深度不可控時改用顯式堆疊。

🔗 相關工具