最小可行模块5分钟可集成但跳过保活与并发写隔离致上线必崩;Upgrade失败因upgrader前误写响应或跨域拒绝,需禁用默认检查并确保Upgrade为handler首操作;WriteMessage非线程安全,须为每连接配send chan统一发送。

能跑通的最小可行模块,5分钟内可集成进现有服务,但跳过关键保活和并发写隔离,上线必崩。
Upgrade失败:403或panic http: multiple response.WriteHeader calls
根本就两件事没做对:upgrader.Upgrade()前不能往w http.ResponseWriter写任何东西(包括log.Println、空格、http.Error),且默认跨域检查会直接拒掉所有前端请求。
- 开发阶段临时解法:
CheckOrigin: func(r *http.Request) bool { return true },但上线必须换成白名单逻辑,比如strings.HasPrefix(r.Header.Get("Origin"), "https://myapp.com") - 确保
Upgrade是 handler 中第一个实际操作,前面只允许路由匹配和简单鉴权 - 如果用了中间件(如日志、JWT校验),确认它没偷偷调用
w.Write或w.WriteHeader
WriteMessage panic: concurrent write to connection
conn.WriteMessage不是线程安全的——多个 goroutine 同时调它,必然触发concurrent write to connection panic 或消息错乱。
- 每个连接配一个
send chan []byte,缓冲大小建议设为256 - 业务逻辑统一走
c.send ,绝不直接调<code>conn.WriteMessage - 起一个专属 writer goroutine,从 channel 读、调
conn.WriteMessage,出错立即break并conn.Close() - 连接关闭前务必
close(c.send),否则 writer 会永远阻塞在<-c.send
连接“在线”却收不到消息:TCP挂着但实际已断
所谓“在线”,只是 TCP 连接还处于 ESTABLISHED 状态,客户端可能早已锁屏、切后台、NAT 超时或进程被杀。此时继续往该 conn 写,会卡住 goroutine,甚至触发net.ErrWriteTimeout。
立即学习“go语言免费学习笔记(深入)”;
- 服务端必须主动发 Ping:
conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(10*time.Second)),每pongWait * 0.9执行一次(如pongWait = 55 * time.Second,则 ping 周期设为50 * time.Second) - 必须设
SetPongHandler,里面重置conn.SetReadDeadline,否则读超时不会刷新 -
conn.SetReadDeadline要在每次conn.ReadMessage前重设,不是只设一次 -
writeWait建议设为10 * time.Second,防止WriteMessage卡死;超时就 close 连接,别等
goroutine 泄漏比内存泄漏更难排查
每个连接至少占两个 goroutine(reader + writer),一旦连接异常关闭(如io.EOF、websocket.CloseAbnormalClosure、net.OpError),而清理逻辑没覆盖到,它们就永远卡在ReadMessage或select上,状态是[IO wait]但不可回收。
- reader goroutine 的 for-loop 必须捕获所有常见错误类型,包括
websocket.IsCloseError(err)、errors.Is(err, io.EOF)、errors.Is(err, net.ErrClosed) - writer goroutine 的 channel 读取必须配合
select+default或timeout,避免死等已关闭的sendchannel - 关闭连接前,先
close(c.send),再conn.Close(),顺序不能反
真正难的不是连上,而是让连接在 NAT、代理、移动端后台限制下持续可靠地双向存活——保活逻辑漏掉任意一环,都可能让服务在凌晨三点开始 silently 失联。


















