核心是三件事:连接升级需设checkorigin避免403,读写必须分离为readpump/writepump两个goroutine,广播时检查conn.isclosed()并设writedeadline超时清理。

直接用 gorilla/websocket 搭一个能跑通、不 panic、不漏消息的最小可行聊天室,核心就三件事:连接升级别卡住、读写必须分离、广播时跳过已断连。
Upgrader.CheckOrigin 为什么必须显式设置
不设 CheckOrigin,浏览器发 ws 请求会直接 403;设成 func(r *http.Request) bool { return true } 是开发阶段最快绕过跨域的方式。但注意:upgrader 默认拒绝所有非同源请求,这个行为不是 bug,是安全默认值。
- 生产环境必须替换为白名单逻辑,比如只允许
https://myapp.com或校验请求头里的X-Auth-Token - 如果前端用了子协议(如
new WebSocket(url, ['json'])),服务端upgrader.Subprotocols必须匹配,否则连接秒关且无明确错误提示 -
Upgrade()调用后,http.ResponseWriter就失效了,不能再往里写东西,否则 panic
为什么 conn.WriteMessage 不能并发调用
*websocket.Conn 的写操作不是并发安全的——多个 goroutine 同时调用 WriteMessage,大概率触发 write tcp: use of closed network connection 或消息错乱(A 用户的消息被 B 收到)。
- 正确做法:每个连接配一个带缓冲的
chan []byte,所有写请求都塞进 channel,再起一个专属 goroutine 顺序消费并调用conn.WriteMessage - 别用
sync.Mutex包裹WriteMessage,锁住整个写流程会导致广播延迟飙升 - 发送前检查
conn.CloseChan()是否已关闭,避免往已关连接发数据引发 panic
广播消息时怎么避免“假在线”连接拖垮服务
所谓“假在线”,是指 TCP 连接还挂着,但客户端早已失联(比如切后台、锁屏、NAT 超时)。这时若广播仍往该连接写,会卡住 goroutine,甚至触发 net.ErrWriteTimeout,最终导致资源泄漏。
- 服务端必须主动发
websocket.PingMessage(比如每 30 秒一次),不能只等客户端 ping - 用
conn.SetPongHandler响应 pong,并在 handler 里重置该连接的活跃计时器 - 每次
WriteMessage前设conn.SetWriteDeadline(time.Now().Add(10 * time.Second)),超时就 close 并从 clients map 中移除 - 广播循环里对每个连接做
if err != nil || conn.IsClosed() { delete(clients, conn); conn.Close() }
最易被忽略的一点:每个连接的 readPump 和 writePump goroutine 必须成对启停。少关一个,就是 goroutine 泄漏;多关一个,可能误杀还在收消息的连接。别依赖 defer,要用显式 channel 通知和 select 控制生命周期。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











