必须显式创建*net.Resolver实例并设PreferGo:true,通过http.Transport.DialContext注入以控制DNS超时、服务器及协议;它不全局生效,也不能直接修改http.DefaultClient或net.DefaultResolver。

怎么用 *net.Resolver 替换默认 DNS 解析器
Go 默认用系统配置(/etc/resolvers 或系统 API)做 DNS 查询,没法控制超时、服务器、协议(如是否走 DoH/DoT),必须显式构造 *net.Resolver 实例并传给 net.Dialer 或直接调用其方法。
关键点:它不全局生效,每次需要解析时得主动用;也不能改 http.DefaultClient 的行为,得自己包装 http.Transport。
-
net.DefaultResolver是包级变量,但它是只读的——不能直接赋值覆盖,只能在局部用新实例 - 必须设
PreferGo: true,否则 Go 会 fallback 到 cgo 的 getaddrinfo,绕过你自定义的Resolver - 超时要靠
net.Dialer.Timeout和net.Dialer.KeepAlive配合,*net.Resolver本身没有超时字段,得靠底层net.Conn控制
如何让 http.Client 走自定义 DNS 解析
HTTP 客户端不认 *net.Resolver,必须通过 http.Transport.DialContext 注入。常见错误是只设了 Dial(旧版函数签名),导致 TLS 握手时仍用系统 DNS。
正确做法是用 DialContext,并在里面调用你自己的 resolver.LookupHost,再拼出 net.Conn。
立即学习“go语言免费学习笔记(深入)”;
- 别漏掉 IPv6 场景:
LookupHost只返回 host 名,实际建连要用LookupIPAddr或LookupNetIP拿到具体 IP+family - 如果后端服务启用了 SNI 或需 IP 直连(如跳过 DNS 负载均衡),记得把原始域名传进
TLSConfig.ServerName,否则证书校验失败 - 复用连接时,
http.Transport会缓存 DNS 结果(默认 0s TTL),可通过Transport.IdleConnTimeout和自定义Resolver的缓存逻辑配合控制
Resolver.LookupXXX 各方法差异和选哪个
Go 标准库提供多个 Lookup 方法,行为差异直接影响结果和性能:
-
LookupHost(ctx, "example.com"):只返回 IPv4/IPv6 地址字符串切片,最轻量,适合只需要 IP 的场景 -
LookupIPAddr(ctx, "example.com"):返回带端口、IP 类型(IPNet)和 hostname 的结构体,适合需区分 v4/v6 或做反向解析的逻辑 -
LookupCNAME(ctx, "example.com"):只查 CNAME 记录,不递归,返回最终别名,注意它不保证目标存在 A/AAAA 记录 -
LookupTXT(ctx, "_acme-challenge.example.com"):查 TXT,ACME 协议常用,但某些 DNS 服务商对短 TTL 或高频查询有限流,需加重试和退避
注意:LookupMX 和 LookupNS 返回的是带优先级或域名的结构体,不是纯字符串,别直接当 host 用。
自定义 DNS 解析容易踩的坑
最常被忽略的是上下文取消传播和错误分类。比如 DNS 查询卡住,但上层 HTTP 请求已超时,此时 ctx 已 cancel,但 resolver 内部可能还在等 UDP 响应。
- UDP 查询没重试机制:标准
net.Resolver对 UDP 失败只报错,不会自动切 TCP;若目标 DNS 服务器丢 UDP 包(如防火墙拦截),得自己封装 fallback 逻辑 - EDNS0 扩展默认开启,某些老旧 DNS 服务器不兼容,会静默丢包;可设
Resolver.StrictErrors = true观察是否因 EDNS 导致失败 - 并发解析大量域名时,
net.Resolver底层用单个net.Conn发 UDP,高并发下易触发io.ErrShortWrite,建议限制 goroutine 并发数或改用连接池化 DNS 客户端(如miekg/dns)
真正难的不是写出来,而是让自定义解析在 timeout、重试、降级、日志追踪这些边界上不掉链子——尤其是混合使用 DoH 和传统 UDP 时,两个路径的错误码、延迟分布、重试策略都得单独对齐。


















