gin.engine 不支持单实例多端口监听,因 r.run() 内部调用阻塞的 http.listenandserve(),连续调用只会执行第一个;需为每个端口创建独立 http.server 并用 errgroup 并发启动。

gin.Engine 本身不支持单实例多端口监听 —— 调用多次 r.Run() 只会阻塞在第一个,后续调用无效;直接起多个 gin.Engine 实例也必须配合独立的 http.Server 和协程,否则仍无法并发监听。
为什么 r.Run(":8080") 和 r.Run(":9090") 不能连着写
r.Run() 内部调用的是 http.ListenAndServe(),该函数会阻塞当前 goroutine 直到服务退出或发生致命错误。所以第二行根本不会执行。常见错误现象是:只看到 [GIN-debug] Listening and serving HTTP on :8080,访问 :9090 直接连接被拒绝。
解决思路是绕过 r.Run(),自己构造 http.Server,并用 go 启动多个监听协程:
- 每个端口对应一个独立的
http.Server实例 - 每个
http.Server的Handler字段设为对应的gin.Engine - 用
golang.org/x/sync/errgroup统一管理多个服务的启停和错误传播(推荐)
用 errgroup.Group 启动两个 Gin 服务示例
这是目前最轻量、无第三方依赖的可行方案:
<pre class="brush:php;toolbar:false;">package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
func router1() http.Handler {
r := gin.New()
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"server": "primary", "port": 8080})
})
return r
}
func router2() http.Handler {
r := gin.New()
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"server": "secondary", "port": 9090})
})
return r
}
func main() {
var g errgroup.Group
// 端口 8080
server1 := &http.Server{
Addr: ":8080",
Handler: router1(),
}
g.Go(func() error {
log.Println("Starting server on :8080")
return server1.ListenAndServe()
})
// 端口 9090
server2 := &http.Server{
Addr: ":9090",
Handler: router2(),
}
g.Go(func() error {
log.Println("Starting server on :9090")
return server2.ListenAndServe()
})
// 阻塞等待任一服务退出(如 Ctrl+C)
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
注意:http.ListenAndServe()
SIGINT 时会返回 http.ErrServerClosed,但 errgroup 默认不区分这个错误 —— 如果你希望进程优雅退出而非崩溃,需在 g.Go 匿名函数里手动忽略该错误。
http.Server 配置项对多端口的实际影响
直接复用同一个 gin.Engine 实例给多个 http.Server 是安全的(gin.Engine 是并发安全的),但每个 http.Server 必须单独配置超时、TLS、Header 限制等参数:
-
ReadTimeout/WriteTimeout:建议显式设置,避免长连接拖垮某一个端口的服务质量 -
TLSConfig:若一个端口走 HTTPS(如:443),另一个走 HTTP(如:8080),必须分别为它们配置不同的http.Server -
IdleTimeout:高并发场景下,不设此值可能导致 TIME_WAIT 连接堆积,尤其在负载均衡后端健康检查频繁时
不要试图共享 http.Server 实例 —— 它的 Addr 字段是只读绑定地址,改了也没用,且会导致 panic。
别踩 gin.Default() 的中间件陷阱
如果你用 gin.Default() 创建多个路由实例,每个都会自带 Logger 和 Recovery 中间件 —— 日志会混在一起,panic 恢复逻辑也彼此隔离。更隐蔽的问题是:gin.Default() 在 debug 模式下会自动注册 /debug/pprof/* 路由,如果两个实例都启用,第二个会因路由冲突 panic(错误信息类似 panic: wildcard route '...' conflicts with existing children)。
建议统一用 gin.New(),按需添加中间件:
- 调试阶段可只在主服务加
gin.Logger() - 所有服务都加
gin.Recovery()是合理的,但确保它们不注册重复的 debug 路由 - 若需指标暴露(如 Prometheus),应使用独立的 metrics server,而非塞进业务端口
多端口不是银弹。真正需要它的场景极少:比如同时暴露管理接口(:9091/metrics)、健康检查(:8080/health)和业务 API(:8000),且明确要求单进程部署。多数时候,用反向代理(Nginx / Traefik)分流更简单、更可观测。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











