应使用 gorilla/websocket 的 upgrader.upgrade() 启动服务,禁用手动 http 响应;读写需分离 goroutine,用 readmessage/writemessage 处理消息,设读写超时,连接断开时主动清理资源。

gorilla/websocket 是当前 Go 生态中最稳妥的 WebSocket 实现,不是“可选”,而是上线前必须用的底层依赖。标准库不支持 WebSocket 协议握手和帧解析,硬啃 RFC 6455 容易在 Sec-WebSocket-Accept 头拼写、base64 编码或掩码处理上翻车,直接导致连接静默失败。
Upgrader 配置必须显式控制跨域与子协议
开发阶段设 CheckOrigin 为 func(r *http.Request) bool { return true } 只能临时绕过,上线后必须校验 r.Header.Get("Origin") 或提取 JWT token 做鉴权。更隐蔽的坑是子协议不匹配:upgrader.Subprotocols = []string{"json"},前端却用 new WebSocket("ws://...", ["v1"]),连接会立刻关闭且无明确错误日志,只在浏览器 DevTools 的 Network 标签下显示 Failed to execute 'send' on 'WebSocket': Still in CONNECTING state。
每个连接必须拆成 readPump + writePump 两个 goroutine
单 goroutine 里混着 ReadMessage 和 WriteMessage 是典型反模式。一旦 WriteMessage 因网络抖动阻塞,整个连接就卡死,后续心跳、消息都发不出去。正确做法是:
-
readPump:循环调用conn.ReadMessage(),解析 JSON 后推入全局broadcastchannel -
writePump:监听该连接专属的sendchannel(带缓冲,如make(chan []byte, 32)),顺序调用conn.WriteMessage() - 两者之间不共享 conn 操作,彻底规避并发写 panic
广播不能起 goroutine 遍历 clients,必须单协程 + channel 串行分发
常见错误是每来一条消息就 go func() { for _, c := range clients { c.send ,用户一多,goroutine 数爆炸,GC 压力飙升,甚至触发 <code>runtime: goroutine stack exceeds 1000000000-byte limit。真正健壮的做法是:
- 所有待广播消息统一发到一个
hub.broadcastchannel - 由单一
hub.run()goroutinefor range broadcast消费 - 遍历
clientsmap 时,对每个 client 的sendchannel 发送——失败则close(c.send)并从 map 中删掉 - 务必在 client 断开时
close(c.send),否则writePump会在select { case msg, ok := 上永久阻塞
最易被忽略的是写操作前没调 conn.SetWriteDeadline(),尤其在 Nginx 反向代理后,proxy_read_timeout 默认 60 秒,服务端若不主动发 PingMessage,连接会被中间件静默 kill,而客户端毫无感知。这个细节不补上,再稳的架构也会在凌晨三点开始掉连接。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











