net.LookupIP无回调机制,需用goroutine+channel或传回调函数封装;必须手动处理TTL缓存、singleflight防击穿,并为Resolver.Dial设置读写超时。

net.LookupIP 不能直接配回调,得自己包一层
Go 标准库的 net.LookupIP 是阻塞同步调用,没有回调机制,也没有内置缓存。想实现“解析完自动触发逻辑”,比如刷新服务列表、通知健康检查模块、写入本地状态,必须在外层封装 —— 不是改标准库,而是用 goroutine + channel 或函数参数传入处理逻辑。
常见错误是试图给 net.LookupIP 加 context.Done() 后直接注册回调,结果发现 context 只控制超时,不提供完成通知。真正可行的做法是:启动 goroutine 调用 net.LookupIP,解析成功后显式调用你传入的回调函数。
- 回调函数签名建议定义为
func(string, []net.IP, error),第一个参数是原始域名,便于区分多个并发查询 - 别在回调里做耗时操作(如写磁盘、发 HTTP 请求),否则会拖慢整个 DNS 查询流程;可只发消息到 channel,由专用 worker 处理
- 如果回调需要访问共享状态(如 map),务必加
sync.RWMutex,读多写少场景下用 RLock/RLock 比 Lock 更高效
本地内存缓存必须管 TTL,否则会返回过期记录
DNS 响应里的 TTL(Time-To-Live)不是装饰字段 —— 它决定了这条记录还能活多久。直接把 net.LookupIP 结果无脑塞进 map[string][]net.IP,等于忽略协议语义,后续可能返回已失效的 IP,导致连接失败或流量误导向。
缓存结构至少得存三样东西:IP 列表、原始 TTL 值、记录插入时间。每次查询前先检查是否过期,过期就清掉重查。不要依赖系统 resolver 的缓存(比如 systemd-resolved 或 macOS mDNSResponder),它们行为不可控、不暴露 TTL、无法与你的回调联动。
- 推荐用
time.Now().Add(time.Duration(ttl) * time.Second)算过期时间点,存进缓存项;查的时候用time.Now().Before(expireAt)判断 - TTL 为 0 意味着“不缓存”,立刻走网络查询,别往内存里写
- 注意:不同记录类型(A/AAAA/MX)TTL 独立,缓存 key 要带记录类型,比如
"example.com:A"
并发查同一域名时,避免重复请求和缓存击穿
高并发下多个 goroutine 同时查 example.com,若各自走网络再各自写缓存,既浪费资源又可能因响应顺序错乱导致缓存值被覆盖。更糟的是,缓存过期瞬间大量请求穿透到上游 DNS,造成雪崩。
免费 DNS 与邮件安全分析(IntoDNS.ai):包括 DNSSEC、SPF、DKIM、DMARC、MTA-STS、BIMI、SMTP STARTTLS、FCrDNS、黑名单、发件人要求及报告。
解决方案不是锁整个缓存 map,而是对每个域名做“单flight”(single-flight)控制:首次请求发起真实 DNS 查询,其余同域名请求等待其结果;结果回来后,统一更新缓存并通知所有等待者。
- 可用
golang.org/x/sync/singleflight,key 用域名 + 记录类型拼接,比如"example.com:A" - singleflight.Do 返回的
val是[]net.IP,err是error,拿到后立刻写缓存并触发回调 - 别把 singleflight.Group 当全局变量反复 new,它本身是线程安全的,复用一个实例即可
Resolver.Dial 必须设读写超时,否则卡死没提示
自定义 net.Resolver 时,很多人只设了 DialContext 函数,却忘了在里面对返回的 net.Conn 设置读写超时。UDP 查询看似简单,但若上游 DNS 服务器丢包或响应慢,conn.Read() 可能永远等下去,整个 goroutine 就卡住,callback 永远不触发。
Go 的 net.Conn 默认不带超时,必须显式调用 SetReadDeadline 和 SetWriteDeadline。用 net.DialTimeout 只解决连接阶段,不解决收包阶段。
- UDP 场景下,在
DialContext返回 conn 后立即执行:conn.SetReadDeadline(time.Now().Add(2 * time.Second)) - TCP 场景同理,且需额外注意:DNS over TCP 可能被防火墙拦截,建议 fallback 到 UDP,而不是死等 TCP 响应
- 如果用了
PreferGo: true,Go 自研解析器仍走 UDP,但底层 Conn 创建逻辑一样,超时设置不能省
实际部署时,最易被忽略的是 TTL 与 callback 的时序配合:缓存刚写入,回调立刻执行,但此时 TTL 可能只剩几秒 —— 如果回调逻辑里做了长周期任务(比如启动一个每分钟轮询的服务实例),得靠自己再校验缓存是否仍有效,不能默认“刚回调就是新鲜的”。


















