Go标准库无原生WebSocket支持,net/http仅提供HTTP升级能力,握手、帧解析、ping/pong、连接状态管理等均需自行实现且极易出错;gorilla/websocket是事实标准,封装RFC 6455全流程,提供Upgrade一行调用、并发安全读写、心跳、压缩及完善错误处理。

为什么直接用 gorilla/websocket 而不是标准库?
Go 标准库没有原生 WebSocket 支持,net/http 只能处理升级请求,但握手、帧解析、ping/pong、连接状态管理都得自己写。不推荐——容易漏掉 CloseMessage 处理或缓冲区溢出导致 panic。用 gorilla/websocket 是事实标准,它把 Upgrade 封装成一行调用,还内置了读写超时、并发安全的 WriteJSON 和 ReadJSON。
安装:
go get github.com/gorilla/websocket
关键点:
-
Upgrader.CheckOrigin默认拒绝跨域,开发时设为func(r *http.Request) bool { return true },上线必须改回校验逻辑 - 每个连接需单独 goroutine 处理读/写,否则一个卡住会阻塞整个连接
-
conn.SetReadDeadline必须设,否则客户端断连后服务端可能永远等不到 EOF
如何安全地广播消息而不 panic?
常见错误是多个 goroutine 同时遍历并写入同一个 map(比如 map[string]*websocket.Conn),或者在遍历时 delete 导致 concurrent map iteration and map write panic。不要用锁包住整个广播循环——写操作会被阻塞,拖慢所有连接。
推荐做法是用通道解耦:
- 定义
type Message struct { Username string `json:"username"` Text string `json:"text"` } - 全局一个
broadcast chan Message,所有连接的读 goroutine 把收到的消息 send 进去 - 单独起一个 goroutine 从
broadcast读,再遍历在线连接列表(用sync.RWMutex保护读) - 对每个连接调用
conn.WriteJSON(msg)前,先检查conn.WriteDeadline并用defer conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
这样读、写、广播三者完全异步,单个连接写失败(如网络中断)只影响自己,不会波及其他。
客户端断连时,服务端怎么及时清理?
WebSocket 没有“心跳超时自动下线”机制。如果客户端直接关浏览器或断网,服务端可能数分钟才收到 i/o timeout 或 use of closed network connection 错误。这时连接还挂在 map 里,用户头像不灰掉,新消息还会往已失效的 conn 写,触发 panic。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
必须主动加心跳:
- 服务端每 30 秒调用一次
conn.WriteMessage(websocket.PingMessage, nil) - 设置
conn.SetPongHandler,收到 pong 就刷新连接的最后活跃时间 - 另起 goroutine 定期扫描连接 map,对超过 60 秒没 pong 的连接,调用
conn.Close()并从 map 中 delete
注意:PingMessage 不会触发 ReadMessage,所以读循环里不用处理 ping;但 PongHandler 必须设,否则默认 pong 会被忽略,心跳检测就失效。
为什么不能把用户名存在 HTTP session 或 cookie?
WebSocket 升级请求是一次性 HTTP 请求,之后通信完全脱离 HTTP 生命周期。session、cookie、header 都只在 upgrade 阶段有效。想传用户名,只有两个可靠方式:
- 在 upgrade URL 里带参数,如
/ws?name=alice,服务端用r.URL.Query().Get("name")提取(注意 URL 解码和长度限制) - 客户端连接成功后,立刻发一条
{"type":"auth","name":"alice"}消息,服务端在读循环开头识别并绑定到该 conn
后者更灵活(支持重命名),但要防重复登录:检查 name 是否已被占用,是则返回错误并 conn.Close()。别用内存 map 做唯一校验——分布式部署时无效,不过单机聊天室够用。
真正难的是消息顺序和离线消息,那得引入 Redis 或消息队列,已经超出“实现一个能跑的聊天室”的范围了。










