upgrade后panic或403的根本原因是:upgrade前向responsewriter写入内容,或未配置checkorigin导致跨域拒绝;writemessage并发调用会触发concurrent write panic,必须用单writer goroutine+channel隔离写操作。

Go 语言里 WebSocket 长连接不是“连上就完事”,而是要主动保活、分离读写、及时清理——否则跑几天就 goroutine 泄漏、连接假在线、广播卡死。
为什么 Upgrade 后立刻 panic 或返回 403
常见现象是浏览器控制台报 ERR_CONNECTION_CLOSED 或服务端 panic:http: multiple response.WriteHeader calls,根本原因就两个:
-
upgrader.Upgrade()前向w http.ResponseWriter写了任何东西(包括空格、换行、log.Println打印到响应体) - 没设
CheckOrigin,默认拒绝所有跨域请求,前端发 ws 连接直接 403
开发阶段最快解法是设 CheckOrigin: func(r *http.Request) bool { return true };但上线必须替换为白名单逻辑,比如只放行 https://myapp.com 或校验 r.Header.Get("Origin")。
conn.WriteMessage 并发调用必 panic
错误现象:多个 goroutine 同时调 conn.WriteMessage,日志出现 concurrent write to connection 或消息错乱(A 用户收到 B 的消息)。
这不是 bug,是设计使然:*websocket.Conn 的写操作非并发安全。正确做法是引入单 writer goroutine + channel:
- 每个连接配一个
send chan []byte(缓冲大小建议 256) - 业务逻辑统一往
c.send 发,不碰 <code>conn.WriteMessage - writer goroutine 从 channel 读、调
conn.WriteMessage,出错立即break并conn.Close() - 务必在连接关闭前
close(c.send),否则 writer 永远阻塞
连接“明明在线却收不到消息”的真实原因
所谓“在线”,只是 TCP 连接还挂着,客户端可能早已失联(锁屏、切后台、NAT 超时)。这时广播仍往该 conn 写,会卡住 goroutine,甚至触发 net.ErrWriteTimeout。
必须双向保活:
- 服务端每
30s调conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(10*time.Second)) - 设
conn.SetPongHandler,收到 Pong 时重置ReadDeadline(比如设为time.Now().Add(55 * time.Second)) - 每次
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:readPump(负责 ReadMessage)和 writePump(负责从 send channel 取消息发出去)。它们生命周期必须严格绑定:
- 不能靠
defer conn.Close()自动清理——它只关连接,不杀 goroutine - 断连信号得走显式 channel(如
closing chan struct{}),readPump和writePump都要select监听它 - 任意一端未退出,goroutine 就永远卡在
ReadMessage或上,状态显示 <code>[IO wait]却不可回收
最易被忽略的是:心跳超时、网络闪断、客户端 kill 进程这些异常场景,io.EOF、websocket.CloseAbnormalClosure、net.OpError 都得覆盖到清理逻辑里,否则资源只会越积越多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











