很多人在做实时功能时都会碰到同一个问题:普通接口要走 HTTP,消息推送或双向通信又要靠 WebSocket,这两套能力能不能放进同一个 Go 服务里一起跑?答案是可以,而且实现方式比不少人想象得更直接。
这篇文章就用一个最小示例把思路拆开:先说明 Go 为什么能同时处理两种请求,再给出可直接运行的代码,最后补上上线前需要注意的几个关键点,方便你判断它是否适合自己的业务场景。
Go 为什么能同时跑 HTTP 和 WebSocket
Go 可以同时提供 HTTP 和 WebSocket 服务,核心不在于启动两套服务器,而在于 WebSocket 本身就是从 HTTP 请求升级而来。
客户端发起连接时,最开始仍然是一个 HTTP 请求,只是会额外携带 Upgrade: websocket 等请求头。服务端识别到这是 WebSocket 握手后,把这条连接升级为长连接;升级完成后,后续通信就不再按普通 HTTP 的请求-响应模式处理,而是进入持续的双向收发。
这也是为什么在 Go 里可以把两种能力放在同一个 net/http 服务中:普通路径继续按 HTTP 处理,指定路径则执行 WebSocket 升级逻辑。它们共用一个端口、一个进程,部署上不需要额外拆分。
最小可运行示例:一个端口同时提供两种服务
安装依赖
示例只需要一个常用库:

go get github.com/gorilla/websocket完整代码
下面这段代码保留了最核心的双服务逻辑,可以直接复制运行:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
func handleWS(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { log.Println(err); return }
defer conn.Close()
for {
mt, msg, err := conn.ReadMessage()
if err != nil { break }
log.Printf("recv: %s", msg)
if err := conn.WriteMessage(mt, msg); err != nil { break }
}
}
func handleHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("HTTP正常 | WebSocket: ws://localhost:8080/ws"))
}
func main() {
http.HandleFunc("/", handleHTTP)
http.HandleFunc("/ws", handleWS)
log.Fatal(http.ListenAndServe(":8080", nil))
}这段代码实际做了什么
代码结构很简单,但把关键路径都覆盖到了。
handleHTTP 负责普通 HTTP 请求,访问根路径 / 时,服务端返回一个状态说明,证明 HTTP 接口可正常工作。
handleWS 负责 WebSocket 请求,访问 /ws 时,通过 upgrader.Upgrade(w, r, nil) 把当前 HTTP 请求升级成 WebSocket 连接。升级成功后,代码进入一个循环:先读取客户端消息,再把原消息写回去,也就是常见的 echo 回声示例。
而在 main() 中,真正启动的仍然只有一个服务:
log.Fatal(http.ListenAndServe(":8080", nil))也就是说,HTTP 和 WebSocket 并不是两个独立监听器,而是由同一个监听端口 :8080 对外提供服务,再根据不同路径分发到对应处理函数。
怎么验证它确实同时生效
验证 HTTP
启动程序后,浏览器访问 http://localhost:8080,如果页面能返回状态提示,说明普通 HTTP 路由已经正常工作。
验证 WebSocket
接着再用支持 WebSocket 的测试工具连接 ws://localhost:8080/ws。连上之后发送任意消息,如果服务端把同样的内容回传回来,就说明升级和双向通信链路都已经打通。
这个测试结果有一个直接意义:同一个 Go 进程、同一个端口,可以同时接住浏览器的普通 HTTP 访问,也可以接住 WebSocket 长连接请求。
它为什么能成立:路由分发加上 Goroutine 并发
从实现上看,Go 的 net/http 更像一个请求入口和路由分发器。
在这个例子里:
/走普通 HTTP 逻辑;/ws走 WebSocket 升级逻辑。
两条路径虽然处理方式不同,但都挂在同一个 HTTP 服务下,因此天然可以共存。
并发方面,Go 的运行时会通过 Goroutine 高效处理大量连接。普通请求来了就进入 HTTP handler,WebSocket 连接建立后则由对应处理逻辑持续收发消息。对于聊天、实时看板、状态推送这类场景,这种模型非常顺手:入口统一,代码不重,扩展也方便。
上生产前,至少要补齐这 3 件事
1. 不要直接保留全开放跨域
示例里这一行:

CheckOrigin: func(r *http.Request) bool { return true }只是为了让本地演示尽快跑通。它表示允许所有来源发起 WebSocket 升级请求,这在生产环境里风险很高。
更稳妥的做法,是检查 r.Header.Get("Origin") 是否命中允许列表,只放行明确受信任的域名。
2. 连接管理不能只靠示例循环
示例中的循环足够说明读写流程,但真正上线时,通常还需要维护连接池,例如用 map 保存活跃连接,并处理以下问题:
- 断线后的清理;
- 客户端重连;
- 心跳检测;
- 无效连接的资源回收。
否则连接一多,很容易出现资源长期占用、状态不一致或广播失败等问题。
3. 库选择要看场景规模
文中的 gorilla/websocket 依然是 Go 生态里非常主流、易用且稳定的方案,对大多数中小规模场景已经足够。
如果业务目标是更高密度的连接数,或者对内存占用更敏感,也可以考虑 gobwas/ws。它的特点是更偏底层、内存开销更低,适合高并发压力更大的场景,甚至可用于百万级连接方向的优化。但从开发效率和可维护性来看,很多项目先用 gorilla/websocket 起步会更实际。
结论:Go 不只能做,而且很适合这么做
回到最初的问题,Go 不仅能同时提供 HTTP 和 WebSocket 服务,而且实现路径非常自然:普通请求交给 net/http,需要实时双向通信的请求则在特定路由上升级为 WebSocket。
从开发角度看,它的优势是结构简单、部署集中、并发处理能力强;从业务角度看,聊天、监控面板、通知推送、协作编辑这类需要实时性的系统,都可以直接沿着这个模式扩展。对于想在一个服务里同时兼顾常规接口和实时通信的项目,这是一种足够轻量、也足够实用的方案。









