Go语言无内置WebSocket支持,生产环境必须用gorilla/websocket;因http.HandleFunc会关闭连接导致升级失败,需在handler中调Upgrade且不再写w;须白名单校验Origin、隔离读写goroutine、手动实现心跳及清理连接。

Go 语言本身不内置 WebSocket 支持,必须用第三方库;生产环境里 gorilla/websocket 是事实标准,不是“可选”,而是“几乎唯一可靠选择”。
为什么不能直接用 http.HandleFunc 注册 WebSocket 路由
因为 http.HandleFunc 的 handler 执行完就关闭底层 TCP 连接,而 WebSocket 升级需要接管该连接的整个生命周期。一旦 handler 返回,HTTP 响应头已发、连接被关,upgrader.Upgrade() 就会失败,浏览器报 ERR_CONNECTION_CLOSED,服务端打日志如 websocket: bad handshake 或 http: multiple response.WriteHeader calls。
正确做法是确保:
- handler 函数内调用
upgrader.Upgrade(w, r, nil)后,不再向w写任何内容(包括空格、换行、fmt.Fprint(w, "")) - 路由注册走
http.ServeMux或直接传给http.Server,别绕过它用http.HandleFunc - 升级前禁止任何中间件提前写响应体(比如日志中间件调了
w.WriteHeader)
upgrader.CheckOrigin 默认拒绝所有跨域请求
开发时设成 return true 很方便,但上线即高危:任意网站都能连你 WebSocket 端点,可能被用于 CSRF 或资源滥用。生产必须显式白名单校验:
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 从
r.Header.Get("Origin")提取来源域名 - 只允许匹配预设列表(如
[]string{"https://myapp.com", "https://staging.myapp.com"}) - 注意协议、端口、子域名都要严格比对,不要用
strings.Contains模糊匹配 - 如果走反向代理(Nginx),确保
X-Forwarded-Proto和X-Forwarded-Host被信任并用于 Origin 构造
并发读写 *websocket.Conn 必须隔离 goroutine
*websocket.Conn 不是线程安全的:多个 goroutine 同时调 WriteMessage() 会 panic concurrent write to connection;同理,ReadMessage() 也不能并发调用。
典型结构是:
- 一个 goroutine 专职
conn.ReadMessage(),收到消息后转发到业务 channel - 另一个 goroutine 专职从
send chan []byte取数据,调conn.WriteMessage() - 断开连接时,先
close(send)让 writer 退出,再conn.Close() - 不要在多个地方保存或复用同一个
*websocket.Conn指针
心跳与连接存活必须手动处理
WebSocket 连接没有内置心跳机制。NAT、防火墙、移动网络常在 60–300 秒无流量后静默断连,客户端收不到错误,conn.ReadMessage() 会一直阻塞或突然返回 io.EOF。
解决方案要同时做两件事:
- 服务端定时发
conn.WriteMessage(websocket.PingMessage, nil)(建议 25 秒间隔) - 客户端必须响应 pong,服务端启用
conn.SetPingHandler(...)并设置conn.SetReadDeadline()防卡死 - 不要依赖
http.Server.IdleTimeout,它对已升级的 WebSocket 连接无效 - 连接断开后,记得从全局
clients map中删掉该*websocket.Conn,否则内存泄漏
真正难的不是连上,而是连得久、断得清、发得准——所有这些都得自己编码兜底,没有魔法开关。


















