GetCertificate 是 tls.Config 的函数字段,用于 TLS 1.2+ 握手时根据客户端 SNI 动态返回匹配证书;必须在多域名单服务、需证书热更新或避免内存常驻全量证书时使用,否则 fallback 到固定证书易致名称不匹配。

GetCertificate 是什么,什么时候必须用它
GetCertificate 是 tls.Config 的一个函数字段,类型为 func(<em>tls.ClientHelloInfo) (</em>tls.Certificate, error)。它只在 TLS 1.2+ 握手阶段、服务器需要根据客户端 SNI(Server Name Indication)提供不同证书时被调用。如果你的 HTTPS 服务要支持多个域名(比如 api.example.com 和 www.example.org),且每个域名用独立证书,又不想提前把所有证书全加载进内存,就得靠它动态查。
不设 GetCertificate 而只设 Certificates 字段,就只能用固定证书——SNI 匹配失败或不带 SNI 的客户端会 fallback 到第一个证书,容易触发证书名称不匹配告警。
实现 GetCertificate 的三个关键动作
- 必须检查
ClientHelloInfo.ServerName 是否为空,空值代表客户端没发 SNI(比如老 HTTP/1.0 工具、某些嵌入式 client),这时返回默认证书或直接 return nil, errors.New("no SNI")
- 查找逻辑不能阻塞:避免在函数体内做同步磁盘读取或网络请求;推荐预先加载证书到 map 或使用带缓存的 LRU 结构
- 返回的
*tls.Certificate 必须由 tls.X509KeyPair 解析得到,不能复用未解析的 PEM 字节或未校验的私钥——Go 不会帮你验证链或时间有效性
ClientHelloInfo.ServerName 是否为空,空值代表客户端没发 SNI(比如老 HTTP/1.0 工具、某些嵌入式 client),这时返回默认证书或直接 return nil, errors.New("no SNI") *tls.Certificate 必须由 tls.X509KeyPair 解析得到,不能复用未解析的 PEM 字节或未校验的私钥——Go 不会帮你验证链或时间有效性 示例片段:
cfg := &tls.Config{
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
name := hello.ServerName
if name == "" {
return defaultCert, nil // 预先解析好的 fallback 证书
}
cert, ok := certCache.Load(name)
if !ok {
return nil, fmt.Errorf("no cert for %s", name)
}
return cert.(*tls.Certificate), nil
},
}常见错误:证书解析时机和并发安全
- 错误地在
GetCertificate 内部每次调用都执行 tls.X509KeyPair(certPEM, keyPEM):这会重复解析 PEM、校验签名、构建证书链,开销大且可能 panic(比如私钥格式错)
- 用
sync.Map 或普通 map 存证书但没加锁:GetCertificate 是被 TLS 库并发调用的,多 goroutine 同时写 map 会 crash
- 忘记处理证书过期:Go 不会在
GetCertificate 中自动校验有效期,得自己在加载时或定期检查 certificate.Leaf.NotAfter
GetCertificate 内部每次调用都执行 tls.X509KeyPair(certPEM, keyPEM):这会重复解析 PEM、校验签名、构建证书链,开销大且可能 panic(比如私钥格式错) sync.Map 或普通 map 存证书但没加锁:GetCertificate 是被 TLS 库并发调用的,多 goroutine 同时写 map 会 crash GetCertificate 中自动校验有效期,得自己在加载时或定期检查 certificate.Leaf.NotAfter 建议做法:
立即学习“go语言免费学习笔记(深入)”;
- 启动时用
tls.X509KeyPair解析所有证书,存入sync.Map或map[string]*tls.Certificate+sync.RWMutex - 加一层简单 TTL 缓存(比如每 5 分钟 reload 一次目录下新证书),避免重启服务
- 对
ServerName做 normalize:转小写、去掉端口(strings.SplitN(hello.ServerName, ":", 2)[0])
为什么不用 tls.ListenAndServeTLS 而要自己启 net.Listener
http.ListenAndServeTLS 内部会新建 tls.Config 并只设置 Certificates 字段,完全忽略你传的 GetCertificate。它只适合单证书场景。
必须手动构造:
ln, _ := tls.Listen("tcp", ":443", cfg)
server := &http.Server{Handler: myHandler}
server.Serve(ln)否则 GetCertificate 永远不会被调用——这个点线上踩过坑的人不少,因为文档没明说限制。
证书热更新本身不难,难的是确保解析、缓存、淘汰、fallback 全链路不出错。尤其当 ServerName 是通配符(*.example.com)时,匹配逻辑得自己写,Go 标准库不提供域名模式匹配。


















