net.lookupip无缓存且不支持预取,每次调用均触发完整dns查询,无ttl管理、无context支持、无法复用结果;需自行封装net.resolver配合sync.map实现带ttl的线程安全缓存。

net.LookupIP 不能缓存,也不支持预取
直接调用 net.LookupIP 是纯同步、无状态、无缓存的系统级查询。它每次都会触发真实 DNS 请求(或走 libc 的有限缓存),Go 层面不维护任何结果复用逻辑,也无法提前发起查询。你传一个域名,它就查一次;再传一遍,照样再查一次——没有 TTL 判断,不读 /etc/hosts,也不管上次结果是否还在有效期内。
常见误操作是试图在循环里反复调用 net.LookupIP("api.example.com") 做“热身”,结果发现每次耗时波动大、QPS 上不去,还容易触发 DNS 限流。这不是 bug,是设计使然:它压根没缓存概念。
-
net.LookupIP返回的[]net.IP不带 TTL 信息,无法判断是否过期 - 它不接受 context,没法做并发预取(比如启动时批量解析一批域名)
- 即使域名刚解析过,下次仍走完整系统 resolver 流程,不会复用
要缓存必须自己封装 Resolver + sync.Map 或第三方库
真正可控的缓存起点是 net.Resolver —— 它支持传 context.Context,能配合 time.AfterFunc 或 cache.LRU 类型结构做 TTL 管理。但标准库不提供缓存实现,得自己补。
典型做法是用 sync.Map 存域名 → []net.IP + 过期时间戳,每次查前先命中判断:
免费 DNS 与邮件安全分析(IntoDNS.ai):包括 DNSSEC、SPF、DKIM、DMARC、MTA-STS、BIMI、SMTP STARTTLS、FCrDNS、黑名单、发件人要求及报告。
- 查前先
Load,若存在且未过期,直接返回 - 否则调
r.LookupIP(ctx, "ip", domain),成功后Store并附带time.Now().Add(ttl) - 注意
net.Resolver实例本身可复用,但缓存 map 必须线程安全 - 别把
net.DNSError也缓存进去——临时错误(如err.(*net.DNSError).IsTemporary())应跳过缓存,避免雪崩
预取只能靠 goroutine + context.WithDeadline 主动触发
Go 没有内置 DNS 预取 API,所谓“预取”就是你在业务逻辑空闲时,用 goroutine 提前查一批已知域名,并把结果塞进缓存。关键点不在查,而在控制时机和失败降级。
- 用
context.WithDeadline(ctx, time.Now().Add(2*time.Second))包裹每个预取请求,防止单个卡住拖慢整体 - 预取列表建议限长(比如最多 10 个),避免 DNS server 被打满或触发 rate limit
- 失败时不 panic,记录日志后继续下一个;预取结果不保证 100% 成功,只是“尽力而为”
- Kubernetes 场景下,Headless Service 的 Pod 域名变化频繁,预取需配合 informer 监听 endpoints 变更再触发
别忽略 TTL 和权威响应的差异
DNS 缓存最易翻车的地方不是代码写错,而是对 TTL 理解偏差:本地缓存的 TTL 来自 DNS 响应包里的 RR TTL 字段,但 net.Resolver 查出来的结果不附带这个值。你得自己从原始 DNS 包解析(用 github.com/miekg/dns)或信任上游 DNS 服务器返回的 TTL。
更麻烦的是,不同 DNS 服务器返回的 TTL 可能不同。比如查 example.com,1.1.1.1 返回 TTL=300,而内网 CoreDNS 返回 TTL=60——你缓存策略若硬写死 300 秒,就可能用到过期 IP。
- 生产环境建议统一走自建 DNS(如 CoreDNS),并配置统一 TTL,降低不可控性
- 若必须混合使用多个 upstream,缓存层得按域名维度隔离存储,避免 TTL 混淆
-
net.DNSError的.IsNotFound()表示 NXDOMAIN(权威回答),可长期缓存(比如 24 小时);而.IsTimeout()是临时故障,绝不缓存
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










