Phase 1: Gin + WebSocket 聊天室

Step 00 — 05 | 從 Hello World 到即時聊天

Gin 基礎

gin.Default() vs gin.New()

gin.Default() = gin.New() + Logger + Recovery middleware。Logger 印每個 request 的資訊,Recovery 在 handler panic 時回 500 不讓 server 掛掉。

gin.Context — 取代 Request + Response

Java (Spring)Go (Gin)
HttpServletRequest + HttpServletResponse*gin.Context
@RequestBodyc.ShouldBindJSON(&obj)
ResponseEntity.ok(data)c.JSON(200, data)
request.getParameter("name")c.Query("name")
@PathVariablec.Param("id")

WebSocket Hub Pattern

聊天室的核心是 Hub — 一個中樞,管理所有連線和訊息轉發。

Client A ──WebSocket──→ readPump ──→ Hub.Broadcast channel Client B ──WebSocket──→ readPump ──→ Hub.Broadcast channel │ Hub.Run() 主迴圈 (select 監聽 3 個 channel) │ 廣播到每個 Client.Send │ writePump ←── Client A.Send writePump ←── Client B.Send │ │ 寫回 WS A 寫回 WS B

select — channel 專用的 switch

select {
case client := <-h.Register:    // 有人加入
case client := <-h.Unregister:  // 有人離開
case message := <-h.Broadcast:  // 有人說話
}

同時監聽多個 channel,誰先有資料就處理誰。Java 沒有對應語法,最接近的是 NIO Selector。

每個連線兩個 goroutine

忘了啟動 writePump 會導致訊息送不出去 — Hub 廣播到 Client.Send 但沒人讀,瀏覽器收不到任何東西。

內層 select 防阻塞

select {
case client.Send <- message:  // 送成功
default:                       // 送不進去 → client 卡住了
    close(client.Send)         // 關掉
    delete(h.Clients, client)  // 踢掉
}

防止一個壞 client 卡住整個廣播迴圈。default 讓它不阻塞。

踩過的坑

StaticFile("/") 路由衝突

Gin 的 StaticFile("/") 會註冊 /*filepath 萬用路由,跟 /ws 衝突導致 panic。改用 StaticFS("/web") 掛在子路徑解決。

Deadlock:select 迴圈自送 Broadcast

在 Register case 裡往 Broadcast channel 送訊息,但消費者也是同一個 select 迴圈 — 死結。解法:Register/Unregister 裡用 sendAll 直接發,不經過 Broadcast channel。

Go 的 time format 用 magic date

不是 yyyy-MM-dd,是固定參考時間 2006-01-02 15:04:05。寫錯數字會被當字面文字。