gorilla/websocket 不能直接用 http.HandleFunc 注册,因其需接管 TCP 连接生命周期,而 http.HandleFunc 返回后 HTTP 连接即关闭,导致 WebSocket 升级失败并报错;正确做法是使用 http.ServeMux 或直接传给 http.Server,并在 handler 中调用 upgrader.Upgrade()。

gorilla/websocket 为什么不能直接用 http.HandleFunc 注册
因为 gorilla/websocket 需要接管底层 TCP 连接的生命周期,而 http.HandleFunc 返回后 HTTP 连接就结束了——WebSocket 升级握手失败,浏览器报 ERR_CONNECTION_CLOSED 或服务端抛 websocket: bad handshake。
正确做法是用 http.ServeMux 或直接传给 http.Server,并在 handler 中显式调用 upgrader.Upgrade():
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true }, // 生产需校验 Origin
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade error:", err)
return
}
defer conn.Close()
// 后续读写...
}- 必须在
Upgrade()前不向w写任何响应体(包括空格、换行),否则触发http: multiple response.WriteHeader calls -
CheckOrigin默认拒绝所有跨域请求,开发时设为return true,生产务必改成白名单逻辑 - 不要在 handler 外部复用
*websocket.Conn,它不是线程安全的,每个连接应独立 goroutine 处理收发
如何安全地并发读写 WebSocket 连接
*websocket.Conn 允许一个 goroutine 调用 WriteMessage(),但不允许并发调用;读操作同理。直接多 goroutine 写会 panic:concurrent write to connection。
典型解法是引入「写队列 + 单 writer goroutine」:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
type Client struct {
conn *websocket.Conn
send chan []byte
}
<p>func (c *Client) writer() {
for message := range c.send {
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
break
}
}
c.conn.Close()
}- 所有业务逻辑通过
c.send <- data发送消息,不直接调用conn.WriteMessage - 读操作也建议单独起 goroutine 调用
conn.ReadMessage(),收到后转发到业务 channel - 注意关闭
sendchannel,否则writer()永远阻塞;通常在连接断开或 client 结构体销毁时close(c.send)
为什么客户端频繁断连却收不到 net.Error
WebSocket 连接静默断开(如 NAT 超时、WiFi 切换)时,conn.ReadMessage() 不会立即返回错误,而是阻塞直到 TCP keepalive 触发(Linux 默认 2 小时),导致服务端长期持有僵尸连接。
必须主动设置读写超时和心跳:
- 用
conn.SetReadDeadline()在每次ReadMessage()前设超时(例如 30 秒),超时后返回net.ErrTimeout - 配合 ping/pong:服务端调用
conn.SetPingHandler(),客户端定时发ping,服务端自动回pong;若连续几次没收到ping,主动conn.Close() - 避免用
time.AfterFunc单独起 goroutine 心跳——容易与连接生命周期脱钩,推荐在writer()中混入conn.WriteMessage(websocket.PingMessage, nil)
gorilla/websocket 和标准库 net/http 的 TLS 兼容性问题
直接把 *websocket.Conn 丢进 HTTPS server 没问题,但要注意:如果用了自签名证书或私有 CA,在客户端(尤其是 iOS WebView 或某些安卓 WebView)可能握手失败,错误信息类似 websocket: bad handshake 或空响应。
- 服务端启动时确保
http.Server.TLSConfig正确加载了证书链(Certificates字段非空,且ClientCAs/ClientAuth按需配置) - 测试时用
wscat -c wss://your-domain/ws,比浏览器更早暴露 TLS 层问题 - Go 1.19+ 默认启用 ALPN,但老版本 Nginx 反代 WebSocket 时若未配
proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade";,会导致升级失败而非 TLS 错误,现象一样但根因不同
实际部署时最常被忽略的是连接生命周期与 goroutine 泄漏的耦合——比如忘记在 reader() 退出时关闭 send channel,或者没用 context.WithTimeout 控制握手阶段耗时,这些都会让连接卡在中间态,慢慢吃光文件描述符。

















