必须用gorilla/websocket,因标准库不支持WebSocket帧通信;本地前端连不上ws://localhost:8080/ws主因是Go默认绑定localhost仅响应127.0.0.1,需改用0.0.0.0:8080监听,Nginx代理须透传Upgrade和Connection头,前端URL必须为ws://或wss://,Upgrader.CheckOrigin默认拒绝跨域需显式配置,ReadTimeout/WriteTimeout须设为0禁用,读写需分离goroutine,关闭前需检查CloseMessage,高频小消息应复用PreparedMessage,心跳优先用WriteControl,WriteBufferPool关闭更稳,http.Client不可千级复用,握手后通信完全脱离HTTP栈。

必须用 gorilla/websocket,标准库不支持 WebSocket 帧通信,别试图用 net/http 自己读写消息。
为什么本地启动后前端连不上 ws://localhost:8080/ws
常见现象是浏览器 Network 里显示 ERR_CONNECTION_REFUSED 或一直 pending。这不是代码逻辑错,而是服务根本没监听到请求。
- Go 默认绑定
"localhost:8080",只响应来自 127.0.0.1 的连接;手机、Docker 容器或局域网其他设备访问时会失败 - 正确做法是监听
"0.0.0.0:8080":http.ListenAndServe("0.0.0.0:8080", nil) - 如果用了 Nginx 反向代理,必须透传两个关键 header:
Upgrade和Connection,漏一个都会卡在 handshake 阶段 - 前端 URL 必须是
ws://或wss://,写成http://不会触发升级流程,控制台也看不到Sec-WebSocket-Key
Upgrader.Upgrade() 报 http.ErrUpgradeRequired 怎么办
这个错误说明客户端请求没带合法的升级头,Upgrade() 直接拒绝了,根本没进业务逻辑。
- 检查前端是否真调用了
new WebSocket("ws://..."),而不是用fetch或XMLHttpRequest - 用
curl手动测试时,必须显式加头:-H "Upgrade: websocket" -H "Connection: Upgrade" -
Upgrader.CheckOrigin默认返回false,跨域请求会被静默拦截;开发阶段可临时设为func(r *http.Request) bool { return true },但上线前必须校验r.Header.Get("Origin")并白名单比对 - 别每次请求都 new 一个
Upgrader实例——它带状态(比如CheckOrigin回调),应定义为包级变量复用
连接建立后秒断,日志出现 read: connection reset by peer
WebSocket 是长连接,但 Go 的 http.Server 默认启用了 ReadTimeout 和 WriteTimeout,空闲几秒就直接 kill 连接。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 必须禁用这两个超时:
srv := &http.Server{Addr: ":8080", Handler: nil, ReadTimeout: 0, WriteTimeout: 0} - 升级后的
*websocket.Conn要自己管心跳和 deadline:conn.SetReadDeadline()和conn.SetWriteDeadline()才有效 - 读写必须分离到不同 goroutine:一个只负责
conn.ReadMessage(),另一个只负责conn.WriteMessage();混在一个函数里顺序执行,第二次读就会阻塞 - 收到
websocket.CloseMessage类型消息时,别再调WriteMessage(),否则报use of closed network connection
压测时 CPU 飙高、GC 频繁,runtime/pprof 显示大量 websocket.preparedMessage
这是典型的消息分配滥用。每次 WriteMessage() 都会拷贝 payload 并封装新对象,高频小消息(如心跳)会迅速拖垮性能。
- 固定内容(如
"ping")提前构建websocket.PreparedMessage实例,全局复用 - 优先用
conn.WriteControl(websocket.PingMessage, nil)发心跳,零分配、不走 message 编码路径 - 关闭
WriteBufferPool(设为nil)反而更稳:高并发下 pool 的锁争用常比直接分配还重 - 别复用同一个
http.Client实例发起千级并发 dial —— 每个 goroutine 应配独立net.Dialer,避免锁瓶颈和端口耗尽
最易被忽略的是:WebSocket 握手只是起点,后续所有帧通信完全脱离 HTTP 协议栈。*http.Request 和 http.ResponseWriter 在 Upgrade() 返回后就彻底失效,任何基于它们的中间件或日志逻辑都得切到 *websocket.Conn 上重新设计。

















