具代表性的面試主題

程式設計面試:如何用 Go testing/synctest 測試非同步程式?

程式題困難
Offer.cc 編輯團隊發佈 更新

題幹

請測試一個帶逾時、背景重試與 context.AfterFunc 的 Go 函式。測試必須快速、可重複,不能依賴真實 sleep;請說明 testing/synctest 的 bubble、虛擬時鐘、Wait、取消語義,以及哪些情況仍需真實整合測試。

題幹與適用場景

被測函式收到請求後啟動背景 goroutine,失敗時指數退避重試,逾時後透過 context.AfterFunc 記錄結果。傳統測試依賴 time.Sleep,偶爾逾時或留下背景任務。請使用 Go 的 testing/synctest 設計確定性測試,並說明實驗版與穩定版 API、goroutine 洩漏、真實 I/O 與時鐘邊界。

題設 API 與版本需按目標 Go 版本確認。題目適合後端程式設計、並行函式庫與基礎設施職位。核心能力是並行測試與時間控制,因此歸為 coding

面試官考察點

第一,能否理解 synctest bubble。bubble 為測試建立隔離的 goroutine 與時間環境,Wait 等待其中背景活動達到 quiescence。

第二,能否區分虛擬時間與真實時間。bubble 內的 time API 可推進虛擬時鐘,但真實網路、檔案或外部 goroutine 不會自動變成確定性。

第三,能否測試取消與清理。context 取消、AfterFunc 回呼與重試 goroutine 都必須在測試結束前完成或明確終止。

第四,能否處理版本差異。Go 1.24 的 synctest 需要實驗開關且 API 不穩定,Go 1.25 提供正式 API;測試與 CI 必須鎖定版本。

第五,能否保留真實整合覆蓋。虛擬時間測試驗證邏輯時序,不能取代 HTTP、資料庫、排程器與 race detector 的真實鏈路測試。

回答前需要釐清的問題

  • CI 使用 Go 1.24 實驗 API 還是 Go 1.25 穩定 API?
  • 被測 goroutine 是否全部在 bubble 內建立?
  • 重試等待使用 time.After、timer 還是外部排程器?
  • 取消後是否允許目前 I/O 完成,還是必須立即返回?
  • 需要驗證 race、真實網路延遲與資料庫鎖嗎?
  • 測試能否注入時鐘或依賴介面,還是只能包住現有函式?

30 秒回答框架

「我會在 synctest.Test bubble 內啟動被測函式,使用虛擬時間推進逾時與退避,再用 synctest.Wait 等待 goroutine 靜止。斷言重試次數、最終錯誤、AfterFunc 是否只執行一次、取消後是否沒有殘留 goroutine。Go 1.24 需實驗開關,Go 1.25 API 已穩定,因此 CI 固定版本。真實 HTTP、資料庫、排程器與 race detector 另跑整合測試。」

分步驟深入解答

第一步:確定版本與 API

Go 1.24 的 testing/synctest 是實驗套件,需要 GOEXPERIMENT=synctest 才可見;Go 1.25 將它作為標準 API,函式名為 TestWait。先鎖定 go.mod、CI 映像與命令環境,避免本地與 CI 使用不同語義。

第二步:把測試放進 bubble

在 bubble 中建立被測 goroutine,避免測試函式外啟動不可控背景任務。bubble 結束前要讓所有 goroutine 返回;取消函式、關閉通道與資源清理放在同一作用域。

go
func TestRetryTimeout(t *testing.T) {
  synctest.Test(t, func(t *testing.T) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    got := runWithRetry(ctx)
    synctest.Wait()
    // Advance virtual time and assert retry/timeout state here.
    _ = got
  })
}

第三步:推進虛擬時間

測試不應呼叫真實 time.Sleep 等待退避。讓 bubble 內時間推進到下一個 timer 或明確 deadline,再呼叫 Wait 讓可執行 goroutine 執行。每次推進後斷言狀態,避免一次跳過多個邊界而難以定位失敗。

第四步:驗證 AfterFunc 與取消

context.AfterFunc 會在取消時啟動回呼 goroutine。測試要記錄回呼次數、回呼是否觀察到預期錯誤,並在取消後 Wait。若被測函式重複註冊回呼,斷言取消路徑只產生設計數量的副作用。

第五步:覆蓋重試與最終結果

注入可控失敗序列,例如兩次失敗後成功,斷言每次退避時間與呼叫次數。再測 deadline 到期、父 context 取消與永久失敗,確保錯誤分類穩定,重試不會在取消後繼續。

第六步:檢查 bubble 邊界

在 bubble 外建立的 goroutine、系統呼叫、真實網路與第三方 runtime 可能不使用虛擬時鐘。將這些依賴替換成介面或在獨立整合測試執行,不能把 bubble 內的確定性當成端到端保證。

第七步:組合其他驗證工具

synctest 適合邏輯時序;go test -race 檢查資料競爭,真實 HTTP/資料庫測試檢查連線、逾時與協定行為。記錄測試時長、失敗重試位置與 goroutine 收斂狀態,防止測試變慢或洩漏。

高品質示範回答

「我先鎖定 Go 版本。Go 1.24 的 synctest 需要實驗開關,Go 1.25 使用正式 synctest.Testsynctest.Wait。在 bubble 內建立所有被測 goroutine,注入失敗序列,推進虛擬時間觸發退避與 deadline,每次推進後等待 quiescence,再斷言呼叫次數、錯誤、AfterFunc 回呼與取消清理。

我不會用真實 sleep,也不把 bubble 外的網路、資料庫或第三方 goroutine 當成虛擬時間的一部分。那些路徑用介面替身做邏輯測試,再用真實整合測試與 go test -race 驗證協定、鎖與資料競爭。測試結束前必須確認 bubble 內沒有殘留任務。」

常見錯誤

  • 在 bubble 中繼續 time.Sleep 測試變慢且失去確定性 → 推進虛擬時間。
  • 只斷言最終結果 → 重試次數與取消清理可能錯誤 → 逐步斷言 timer、呼叫與回呼。
  • 在 bubble 外啟動 goroutine → 無法等待收斂 → 讓任務屬於同一 bubble。
  • 把真實網路當虛擬時間 → I/O 延遲仍不穩定 → 使用替身並獨立跑整合測試。
  • 忽略 1.24/1.25 API 差異 → CI 與本地編譯不一致 → 固定 Go 版本與實驗開關。
  • 不執行 race detector → 時序正確仍可能有資料競爭 → 組合 go test -race
  • 取消後仍重試 → 資源與請求持續增加 → 每個等待與呼叫前檢查 context。
  • 用一次 Wait 代替所有斷言 → 失敗邊界難定位 → 分階段推進與驗證。

追問及應對

追問一:synctest 是否取代真實時間測試?

不能。它驗證可控的並行邏輯與時間關係;系統時鐘、網路、資料庫與排程器仍需真實環境測試。

追問二:為什麼要呼叫 Wait?

虛擬時間推進只讓 timer 到期,Wait 讓 bubble 內可執行 goroutine 執行到靜止狀態,便於穩定斷言。

追問三:如何測試永久阻塞?

為被測操作設定 deadline,推進到 deadline 後斷言錯誤與清理。對無法取消的外部阻塞,用介面替身或獨立逾時測試。

追問四:Go 1.24 實驗 API 能否直接用於生產?

可用於受控測試,但要明確實驗狀態、開啟方式與升級風險。若要求穩定相容,優先 Go 1.25 正式 API 並固定 CI。

追問五:如何發現 bubble 外洩漏?

在測試結束前檢查任務完成訊號與資源關閉,再結合 -race、goroutine profile 或整合測試逾時。synctest 不能取代所有程序級洩漏偵測。

追問六:如何驗證重試退避沒有漂移?

記錄每次呼叫的虛擬時間,與預期退避序列逐項比較;同時測試 deadline 截斷最後一次等待與取消提前終止。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

截圖題目後,依序看約束、解法、程式碼、邊界條件和複雜度。

查看工具