websocket写消息前必须检查连接状态,否则writemessage会panic;conn.remoteaddr()不可靠,应设写 deadline 并捕获net.errclosed/io.errunexpectedeof,或由读协程关闭连接后通知写协程。

WebSocket 写消息前不检查连接状态,WriteMessage 就会 panic
Go 的 websocket.Conn 不是线程安全的“活连接”抽象,它底层绑定了一个已关闭的 TCP 连接时,WriteMessage 会直接 panic 或返回 write: broken pipe。这不是偶发错误,而是必然结果——只要客户端刷新页面、断网、或标签页休眠超时(Safari 常见),服务端还拿着旧 *websocket.Conn 往里写,就崩。
- 别依赖
conn.RemoteAddr() != nil判断是否可用,这个值在连接关闭后仍可能非空 - 真正可靠的判断是:在
writePump中调用前加conn.SetWriteDeadline(time.Now().Add(3 * time.Second)),再捕获net.ErrClosed和io.ErrUnexpectedEOF - 更推荐做法:读协程监听到
io.EOF或websocket.CloseMessage后,立刻conn.Close()并关闭该连接专属的chan []byte,写协程用select { case msg, ok := 安全退出
SSE 推送失败八成是因为没 flush,data: 写了但浏览器收不到
EventSource 协议要求服务端持续流式输出,并且每次 data: 行后必须跟两个换行符 \n\n,同时响应体不能被缓冲卡住——否则浏览器永远等不到完整事件块,报错 net::ERR_INCOMPLETE_CHUNKED_ENCODING。
- 必须设置三组响应头:
w.Header().Set("Content-Type", "text/event-stream")、w.Header().Set("Cache-Control", "no-cache")、w.Header().Set("Connection", "keep-alive") - 每次写完都要显式
w.(http.Flusher).Flush();用json.NewEncoder(w).Encode()会失效,因为没控制换行和格式 - 正确写法:
fmt.Fprintf(w, "data: %s\n\n", msg); w.(http.Flusher).Flush()
用 map[*websocket.Conn]bool 管理连接,高并发下必 crash
原生 Go map 不支持并发读写。当多个 goroutine 同时执行 clients[conn] = true(注册)和 delete(clients, conn)(断开),或者广播时遍历 + 删除混用,就会触发 fatal error: concurrent map read and map write。
- 生产环境必须换成
sync.Map,或自己加sync.RWMutex包裹操作 - 更稳妥的是把连接增删逻辑收口进一个 hub goroutine,用
chan通信:register chan *websocket.Conn、unregister chan *websocket.Conn、broadcast chan []byte - 别在
defer里直接delete(clients, conn),要先发信号给 hub,再等确认注销完成
Upgrader.CheckOrigin 设成 return true 是裸奔,但硬匹配又拦不住合法请求
前端部署在 https://admin.example.com,测试环境在 http://localhost:3000,而微信小程序 WebView 又带自己的 Origin —— 全部放行危险,全部拒绝用不了。
- 简单安全方案:
CheckOrigin: func(r *http.Request) bool { origin := r.Header.Get("Origin"); return strings.HasSuffix(origin, ".example.com") || origin == "http://localhost:3000" } - 如果用了 Nginx 反向代理,记得配
proxy_set_header Origin $http_origin;,否则Origin头会被吃掉 - 别在升级前手动读
r.Body,Upgrader.Upgrade内部会重置 request,提前读会导致后续解析失败
WebSocket 的心跳、SSE 的 flush、连接管理的并发安全——这些不是“附加功能”,是只要上线就会暴露的硬性门槛。没人会在压测时才想起写 SetPingHandler,就像没人真信“先跑起来再优化”能扛住真实用户切页、断网、重连的组合拳。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











