Files
cs-note/hzh/GIN/15-advanced-running.md
2026-05-24 11:42:38 +08:00

5.3 KiB
Raw Permalink Blame History

tags, create time
tags create time
后端
Go
Gin
服务器
多服务
2026-04-28 00:00

高级运行场景

概述

除了一台服务器上跑一个单一服务之外,还有一些进阶场景:单进程监听多个端口、同时运行 gRPC 和 HTTP 服务、HTTP/2 Server Push 等。这些场景虽然不一定常用,但在特定架构需求下非常关键。

思考题:为什么在同一进程中同时监听 HTTP 和 gRPC 比拆成两个服务更好?又有哪些代价?

正文

1. 单进程多端口监听

一个 Engine 实例可以绑定多个地址:

func main() {
    // 主服务 — 对外 API
    go func() {
        srv := &http.Server{Addr: ":8080", Handler: gin.Default()}
        log.Fatal(srv.ListenAndServe())
    }()

    // 内部服务 — 管理后台或健康检查
    internal := gin.New()
    internal.GET("/admin/health", adminHealth)
    internal.GET("/admin/metrics", adminMetrics)
    go func() {
        srv := &http.Server{Addr: ":9090", Handler: internal}
        log.Fatal(srv.ListenAndServe())
    }()

    // 阻塞
    select {}
}

2. HTTP + gRPC 共存

在一个进程内同时提供 RESTful HTTP 和 gRPC 服务——共享同一个 Engine 实例和配置:

func main() {
    r := gin.Default()

    // 注册 HTTP 路由
    r.GET("/api/users", listUsers)
    r.POST("/api/users", createUser)

    // gRPC server
    grpcServer := grpc.NewServer()
    pb.RegisterUserServiceServer(grpcServer, &userGRPCServer{})

    // HTTP + gRPC 共享同一个端口 —— 通过 HTTP Upgrade 协议协商
    lis, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatalf("listen failed: %v", err)
    }

    // HTTP/2  multiplexing:gRPC 要求 HTTP/2
    mux := http.NewServeMux()
    mux.Handle("/", r)           // Gin 处理普通请求
    mux.Handle("/_grpc_", grpcHandler) // gRPC 处理 gRPC 请求

    // 注意:真正的 HTTP/2 mux 需要更复杂的配置
    // 推荐使用 github.com/bazelbuild/rules_go/go/runfiles 或其他 multiplexer
    // 最简单的方式是分别监听不同端口
    log.Println("HTTP on :8080, gRPC on :50051")

    go func() {
        if err := http.ListenAndServe(":8080", r); err != nil {
            log.Fatalf("http failed: %v", err)
        }
    }()

    go func() {
        lis, _ := net.Listen("tcp", ":50051")
        if err := grpcServer.Serve(lis); err != nil {
            log.Fatalf("grpc failed: %v", err)
        }
    }()

    select {}
}

两种部署策略对比:

策略 优点 缺点
同进程不同端口 共享配置、部署简单 端口占用
Nginx 反代 协议隔离清晰 网络跳数增加
gRPC-Gateway 统一入口、单一端口 增加复杂度

3. HTTP/2 Server Push

Go 的 net/http 原生支持 HTTP/2 Push(Gin 作为 http.Handler 也继承了这个能力),但现代浏览器已不再推荐:

func pushHandler(c *gin.Context) {
    pusher, ok := c.Writer.(http.Pusher)
    if !ok {
        c.String(http.StatusOK, "push not supported")
        return
    }

    // 推送 CSS 文件给浏览器(不等 JS 请求后再传)
    if err := pusher.Push("/static/style.css", nil); err != nil {
        log.Printf("push failed: %v", err)
    }

    c.HTML(http.StatusOK, "index", gin.H{"title": "Home"})
}

现状提示: Chrome/Safari 已于 2017-2018 年废弃 HTTP/2 Push 的支持。MDN 明确标注该 API 为 deprecated。不建议在新项目中使用。

4. HTTP/3 (QUIC) 支持

Go 1.21+ 开始实验性支持 QUIC(UDP-based HTTP/3),但不需要额外配置——只要你的服务端启用了 TLS 并且客户端支持 HTTP/3,Go 会自动协商:

srv := &http.Server{
    Addr:    ":443",
    Handler: r,
}
// Go 自动尝试使用 QUIC listener
// 前提:启用 TLS(HTTP/3 仅适用于 HTTPS)
srv.ListenAndServeTLS("", "")

5. 运行时动态切换端口

有时需要根据配置或环境变量动态调整监听端口:

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    addr := ":" + port

    // 优雅切换端口:先 shutdown 旧 server,再启动新 server
    // (见 [[13-graceful-shutdown]])
    switchPort(addr)
}

6. 条件启动(Feature Flag)

根据配置决定是否开启某些路由或服务:

func setupRoutes(r *gin.Engine) {
    r.GET("/ping", pingHandler)

    // 只有开启了 feature flag 才注册实验性功能
    if os.Getenv("ENABLE_EXPERIMENTAL") == "true" {
        r.GET("/experimental/features", experimentalFeatures)
    }

    // 只在调试模式下注册调试路由
    if gin.Mode() == gin.DebugMode {
        r.GET("/debug/vars", pprof.Index)
        r.GET("/debug/pprof/", pprof.Index)
    }
}

思考题:如果你需要在不停机的情况下动态增删路由,Gin 能做到吗?它的路由树一旦注册后能否删除某条路由?

答案:Gin 的路由注册是单向的——addRoute 只加不减。如果需要在运行时动态管理路由,可以考虑使用独立的 RouterGroup 隔离功能模块,通过标志位控制是否注册该 Group。

关联笔记