dialcontext是唯一可靠入口,因http客户端tls握手前需解析域名,旧版dial无context无法超时控制或注入自定义resolver;dialcontext可拆host、调lookuphost、拨tcp,且必须手动集成自定义resolver,否则仍走系统dns。

HTTP 请求必须走自定义 DNS 时,不能改 http.DefaultClient,也不能只设 Dial;得用 DialContext + 自定义 *net.Resolver 实例,且 Resolver 必须配 PreferGo: true 和 Dial 函数。
为什么 DialContext 是唯一可靠入口
HTTP 客户端在建立连接前会先解析域名,这个过程发生在 TLS 握手之前。旧版 Dial 函数只传 network 和 addr(如 "tcp", "example.com:443"),但不带 context,无法控制超时,也无法注入自定义 resolver 逻辑。而 DialContext 在每次连接前调用,给你完整控制权:拆出 host、调 resolver.LookupHost、再拨 TCP 连接。
- 漏掉
DialContext直接设Dial→ 后续 TLS 握手仍走系统 DNS,自定义失效 - 只设
DialContext但没传自定义 resolver → 默认还是走net.DefaultResolver,也就是系统配置 - 用
http.DefaultClient改 Transport → 不影响已存在的 client 实例,且全局修改易引发竞态
net.Resolver 必须同时满足两个条件才真正生效
*net.Resolver 不是设了就跑,它默认 fallback 到系统 resolver。要让它真正走 Go 原生 DNS 客户端并连指定服务器,必须同时满足:
-
PreferGo: true:启用 Go 自研解析器,绕过 cgo 的getaddrinfo -
Dial字段非 nil:返回一个能连 DNS server 的net.Conn,比如 UDP 连"1.1.1.1:53"
常见错误是只写 PreferGo: true 却忘了 Dial,结果 runtime 仍 fallback 到系统行为,日志里也看不出异常。
正确写法示例:
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
return (&net.Dialer{Timeout: 2 * time.Second}).DialContext(ctx, "udp", "1.1.1.1:53")
},
}
如何让 DialContext 调用你的 resolver
DialContext 是闭包,里面要手动调 resolver.LookupHost,不能指望 Transport 自动识别你设的 Resolver 字段(那个字段只对 net.Dialer 本身生效,和 HTTP 无关)。
- 先用
net.SplitHostPort(addr)拆出 host,别直接拿addr去查(否则会把"example.com:443"当域名) - 调
resolver.LookupHost(ctx, host),它返回[]string,不是[]net.IP,适合拼接ip:port - 拿到 IP 后,用原
dialer.DialContext连ip + ":" + port,不是再用 resolver 查一次 - 记得把原始域名传给
TLSConfig.ServerName,否则证书校验失败(IP 直连时 SNI 仍需原始 host)
片段示意:
dialer := &net.Dialer{Timeout: 3 * time.Second}
resolver := &net.Resolver{ /* ... */ }
<p>transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
ips, err := resolver.LookupHost(ctx, host)
if err != nil {
return nil, err
}
// 取第一个 IP(或加负载逻辑)
return dialer.DialContext(ctx, network, ips[0]+":"+port)
},
}
</p>
容易被忽略的缓存与并发问题
Go 的 *net.Resolver 本身线程安全,可复用,但它的结果不带 TTL 缓存。HTTP Transport 默认也不缓存 DNS 结果(Transport.IdleConnTimeout 控制连接空闲,不控制 DNS)。如果高频请求同一域名,每次都在查 DNS,压力会直线上升。
- 不要自己用
sync.Map缓存LookupHost结果后忽略 TTL —— DNS 记录可能已变,导致服务不可达 - 若需缓存,建议封装一层带 TTL 的 LRUCache,key 是
host,value 是[]string+time.Time过期时间 - Kubernetes Headless Service 场景下,必须用
resolver.LookupIP(ctx, "ip", domain),且传"ip"而非"ip4",否则 IPv6 地址会被过滤掉
最常翻车的地方不是写错函数名,而是 Resolver 配了 PreferGo 却没配 Dial,或者 DialContext 里没拆 host 就直接查带端口的字符串 —— 错误信息永远是 *net.DNSError: no such host,根本看不出是格式问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











