beego本身不内置websocket实现,需依赖gorilla/websocket手动升级连接;路由必须显式绑定get方法,配置checkorigin放行跨域,启用nginx透传upgrade头,并用双goroutine管理读写生命周期。

Beego 本身不内置 WebSocket 实现,必须依赖 gorilla/websocket 手动升级连接;直接调用 beego.Controller 的 Get() 方法处理握手即可,但关键在升级前的配置和升级后的长连接生命周期管理。
beego.Router 映射 WebSocket 路由时必须用 GET 方法
Beego 的路由机制不识别 ws:// 协议,只按 HTTP 方法匹配。WebSocket 握手本质是带 Upgrade: websocket 头的 GET 请求,所以路由必须显式绑定 GET 动作:
-
beego.Router("/ws", &controllers.WsController{}, "get:Handle")是正确写法 - 若写成
"*:Handle"或漏掉"get:"前缀,this.Ctx.Request.Method可能不是GET,导致upgrader.Upgrade()拒绝处理 - 不要在路由里加
/ws/末尾斜杠——浏览器发起new WebSocket("ws://.../ws")时,路径必须严格一致,否则 404
upgrader 配置里 CheckOrigin 默认拒绝跨域,开发阶段要显式放行
前端页面不在 Beego 同域名下(比如 Vue 开发服务器跑在 http://localhost:5173),浏览器会携带 Origin 头,而 gorilla/websocket 的 upgrader 默认校验 Origin,失败直接返回 403。
- 开发时快速解决:设置
CheckOrigin: func(r *http.Request) bool { return true } - 线上环境必须限制:例如
return r.Header.Get("Origin") == "https://your-app.com" - 别忽略
HandshakeTimeout:设为5 * time.Second防止慢客户端卡住 goroutine -
EnableCompression: true可减少文本消息体积,但需客户端支持permessage-deflate
ReadMessage/WriteMessage 必须配对 goroutine,且需主动 close
WebSocket 连接是长生命周期对象,不能像 HTTP 一样函数返回就自动清理。每个连接至少需要两个 goroutine:一个读、一个写,否则容易死锁或 panic。
- 读循环里用
for+ws.ReadMessage(),遇到io.EOF或websocket.CloseMessage时应退出并调用ws.Close() - 写操作不能直接在读循环里同步执行——如果对方网络卡住,
ws.WriteMessage()会阻塞整个 goroutine,导致无法继续读 - 推荐模式:读到消息后发到一个
inChan chan []byte,另起 goroutine 从outChan取数据写入连接;关闭前先关 channel,再ws.Close() - 务必在 defer 或 error 分支调用
ws.Close(),否则连接句柄泄漏,Go runtime 不会自动回收底层 TCP 连接
nginx 反向代理必须透传 Upgrade 和 Connection 头
Beego 应用若部署在 nginx 后,而 nginx 没配透传,握手请求会被当成普通 HTTP,返回 200 而非 101,前端报错 “Error during WebSocket handshake: Unexpected response code: 200”。
- nginx 配置中必须有这两行(在
location /ws块内):proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "upgrade"; - 还要禁用缓存:
proxy_cache_bypass $http_upgrade; -
proxy_read_timeout至少设为 60 秒,避免空闲连接被 nginx 断开(默认是 60 秒,但 Beego 的心跳间隔建议设为 25–30 秒) - 别用
proxy_pass http://127.0.0.1:8080/ws这种带路径的写法——它会重写Request-URI,破坏 WebSocket 握手所需的原始路径
真正难的不是写通第一个消息,而是连接断开时的状态清理、多房间广播的并发安全、以及心跳超时后如何优雅踢出客户端——这些逻辑不会自动发生,得靠你自己用 sync.Map 管理连接池、用 time.AfterFunc 绑定超时回调、并在所有 ws.Close() 前确保已从全局 map 中删掉该连接。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











