生产环境websocket推送需解决连接管理、心跳保活、并发写安全、多端登录互斥、离线兜底五大问题:须用tcp+应用层双心跳、单连接单goroutine顺序写、用户id映射连接列表并主动踢旧连、内存环形缓冲+异步落库、避免handler阻塞。

Go 语言写消息推送服务,核心不是“能不能”,而是“推什么、推给谁、怎么不丢、怎么不压垮自己”——用 net/http + gorilla/websocket 能跑通,但生产环境必须直面连接管理、心跳、重连、离线兜底这四件事。
WebSocket 连接生命周期没接管,用户一刷页面就断连
浏览器 WebSocket 断开时不会自动重连,服务端也默认不感知掉线。靠 TCP 心跳(SetPingHandler)+ 应用层心跳(自定义 pulse 消息)双保险才可靠。
- 服务端必须调用
conn.SetPingHandler并在ReadMessage循环里处理websocket.PingMessage,否则内核 keepalive 不触发,NAT/代理超时后静默断连 - 客户端每 30s 发一次
{"type":"pulse"},服务端收到后立刻回{"type":"pong"};连续 2 次没收到pulse就主动conn.Close() - 别依赖
http.Request.Context().Done()判断断连——它只在 HTTP 握手阶段有效,升级成 WebSocket 后就失效了
并发写 WebSocket 连接 panic: concurrent write to websocket connection
一个 *websocket.Conn 实例**不允许并发调用 WriteMessage**,这是 Go 官方明确禁止的。常见于:后台 goroutine 推送 + HTTP handler 主动发消息同时写。
- 每个连接配一个带缓冲的
chan []byte(比如make(chan []byte, 64)),所有写请求先入队 - 单独起一个 goroutine 从该 channel 读取并调用
conn.WriteMessage,顺序写,天然串行 - 写 channel 前检查
conn.IsClosed(),避免往已关连接发数据导致 panic - 别用
sync.Mutex包裹WriteMessage——锁住写操作会阻塞整个连接,拖慢其他用户
用户登录后换设备,旧连接没踢下线,消息重复推
一个用户 ID 可能对应多个活跃连接(手机、PC、平板),但业务上通常只允许最新登录生效。靠内存 map 管理连接时极易漏清理。
- 用
map[string][]*Client(key 是 user_id)存连接,每次新连接建立时,先遍历旧连接列表,对每个*Client调用conn.Close()并从 slice 中删掉 - 删除操作必须加
sync.RWMutex,读多写少,用RLock/Lock分开控制 - 别等 GC 或超时自动断连——用户可能一直挂着,旧连接持续收消息,造成数据错乱
- 如果用 Redis 存连接状态,注意
DEL和PUBLISH之间有竞态,得用 Lua 脚本保证原子性
HTTP 接口推消息时,大量用户不在线,直接写数据库就卡住
推送请求进来,发现目标用户当前无 WebSocket 连接,就得走离线存储。但同步写 DB + 发 MQ 容易成为瓶颈。
- 离线消息先写进内存 Ring Buffer(如
github.com/cespare/xxhash+sync.Pool管理小对象),再异步批量刷到 Redis Stream 或 Kafka - HTTP handler 里只做轻量判断:
if client, ok := clients[uid]; ok { client.send(msg) },其余全扔给后台 worker - Redis Stream 的
XADD要设MAXLEN ~1000防爆内存,过期用XTRIM+ 定时任务清理 - 别在 HTTP handler 里调
http.Post推送 APNs/极光——网络延迟不可控,超时会拖垮整个接口
真正难的不是写通 WebSocket,是让每个连接活够 30 分钟、消息不重复不丢失、百万连接下内存不爆、扩容时连接不抖动——这些都藏在心跳策略、连接池复用、离线队列分片和信号量限流的细节里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











