select 的 case 後面不是條件而是一個 channel

看到 case <-time.After(2 * time.Second): 最直覺的誤讀是「如果兩秒到了」。不是。case 後面永遠是一根 channel,select 只會等管子,不會判斷條件。

拆成兩行就清楚了,兩種寫法完全等價

// 寫法 A:常見寫法
select {
case msg := <-ch:
case <-time.After(2 * time.Second):
}
 
// 寫法 B:拆開
timer := time.After(2 * time.Second)   // 先呼叫函式,拿到一根 channel
select {
case msg := <-ch:
case <-timer:                          // select 等的是這根 channel
}

為什麼「跑到 select 了還要執行 time.After」

因為 select 必須先知道自己要等哪些 channel,才能開始等。程式跑到 select 的第一件事,是把每個 case 裡的 channel 運算式由上到下算出來一次(見 select 分三階段:求值全部、等待全部、只執行一個)。

所以問題不是「跑到 select 了,他為什麼還要執行 time.After」,而是**「select 為了知道要等什麼,一定得先執行 time.After 拿到那根管子」**。

time.After 是普通函式,0 秒就回傳

start := time.Now()
timer := time.After(2 * time.Second)
fmt.Printf("[%.1fs] 已回傳,型別是 %T\n", time.Since(start).Seconds(), timer)
t := <-timer   // ← 卡住的是這裡
fmt.Printf("[%.1fs] 拿到了\n", time.Since(start).Seconds())
[0.0s] 已回傳,型別是 <-chan time.Time
[2.0s] 拿到了

time.After 做三件事:建立 channel → 請背景計時器 2 秒後往裡面丟一個時間值 → 立刻回傳 channel。卡住的是接收動作 <-timer,不是 time.After 本身。往管子送資料的也不是你的程式碼,是 Go runtime 的計時器。

一句話

time.After 不是條件判斷,是一根「2 秒後才會有水流出來的管子」。select 只會等管子,所以要讓 select 懂「逾時」,就得先把時間變成一根管子。

<- 是接收運算子,箭頭方向就是資料流向:ch <- "hi" 送進去、msg := <-ch 拿出來;case <-time.After(...) 只在乎「鬧鐘響了沒」,不在乎那個時間值。

相關:for-select 裡的 time.After 每圈重置select 是 channel 版的 switch