根本原因是未启用TLS:Gin仅通过RunTLS或手动http.Server+ListenAndServeTLS启动HTTPS时才支持HTTP/2,且需证书支持ALPN协商"h2";调用Run则底层为http.ListenAndServe,强制HTTP/1.1。

为什么 Gin 启动 HTTPS 后还是 HTTP/1.1?
根本原因只有一个:没走 TLS。Go 的 net/http 对 HTTP/2 有硬性要求——必须通过 ListenAndServeTLS(或等效封装)启动,且 TLS 配置需支持 ALPN 协商 "h2"。Gin 本身不干预协议协商,它只是把 Handler 交给标准库的 http.Server;你调用 r.Run(":8080") 或 r.Run("localhost:8080"),底层就是 http.ListenAndServe,永远只能是 HTTP/1.1。
- 检查是否用了
r.RunTLS(":8443", "cert.pem", "key.pem")—— 这是最简路径,但证书路径必须可读、私钥不能加密、权限得是0600 - 若用自签名证书,
curl --http2 https://localhost:8443可能失败,因为 Chrome/Firefox 直接拒绝;建议用mkcert生成本地可信证书 - 别在反向代理(如 Nginx)后只监听 HTTP 端口再转发——HTTP/2 无法穿透非 TLS 代理,客户端看到的仍是 HTTP/1.1
Gin 的 RunTLS 和手动配置 http.Server 有什么区别?
r.RunTLS 是封装好的快捷方式,适合开发和轻量部署;手动构造 http.Server 则用于需要精细控制 TLS 行为的场景,比如强制 TLS 版本、禁用弱密码套件、启用客户端证书验证等。
-
r.RunTLS(":443", "fullchain.pem", "privkey.pem")内部会新建http.Server并调用srv.ListenAndServeTLS,但不暴露TLSConfig修改入口 - 若要设
TLSConfig.MinVersion = tls.VersionTLS12或补全证书链,必须绕过RunTLS,自己写:srv := &http.Server{Addr: ":443", Handler: r, TLSConfig: &tls.Config{...}},再调用srv.ListenAndServeTLS(...) - 证书链不完整(比如只给了域名证书,没含中间 CA)会导致浏览器报
NET::ERR_CERT_AUTHORITY_INVALID,此时应合并为fullchain.pem
如何确认 HTTP/2 真正在工作?
别信日志或文档描述,直接看客户端行为。Go 标准库不把协议版本塞进 http.Request.Proto(它只反映请求行里的字符串,不是实际协商结果),所以得靠外部工具验证。
- 用 curl 测试:
curl -I --http2 https://localhost:8443/ping—— 成功时首行显示HTTP/2 200;失败则回落到HTTP/1.1 - Chrome DevTools → Network → 点开任意请求 → Headers → 查
Protocol列是否为h2 - 注意 curl 版本:macOS 自带 curl 通常不支持 HTTP/2,需
brew install curl-openssl替换,否则--http2参数无效
HTTP/2 多路复用失效的常见陷阱
即使服务端启用了 HTTP/2,客户端若配置不当,仍可能退化为每个请求建新 TCP 连接,完全浪费多路复用能力。
立即学习“go语言免费学习笔记(深入)”;
- 微服务间调用时,别每次请求都 new 一个
http.Client;http.Client的Transport是线程安全的,应全局复用 -
http.Transport.MaxIdleConnsPerHost必须 ≥ 1(建议设为100),否则连接池按 Host 隔离,无法跨请求复用连接 - Handler 中避免大块阻塞写入:比如
w.Write(largeBuf)会卡住整个流,其他并发请求被迫等待;改用io.Copy分块传输 - 中间件若读取了
r.Body(如鉴权、日志),必须用r.Body = ioutil.NopCloser(bytes.NewReader(buf))还原,否则后续 Handler 读不到 Body,静默失败


















