net.lookupip 返回的 ip 顺序不可靠,因其底层调用系统 getaddrinfo/getaddrinfow,受 /etc/gai.conf、ipv6 接口状态和 dns 响应中 a/aaaa 记录顺序共同影响,导致结果随缘而非自适应。

Go 的 net.LookupIP 默认不区分 IPv4/IPv6,返回结果顺序依赖系统 DNS 解析器和本地协议栈配置,不是“自适应”,而是“随缘”。你得自己筛、自己选、自己兜底。
为什么 net.LookupIP 返回的 IP 顺序不可靠
它底层调用的是系统 getaddrinfo(Unix)或 GetAddrInfoW(Windows),受三个因素共同影响:/etc/gai.conf(Linux)、IPv6 接口状态、DNS 响应中 A/AAAA 记录的排列顺序。比如某次查 google.com 返回 [216.58.221.14],下次可能变成 [2607:f8b0:4009:815::200e],甚至混排 —— 这不是 bug,是标准行为。
常见错误现象:
• 服务启动时连上 IPv6 地址,但本地没配 IPv6 路由,连接超时
• HTTP 客户端用第一个 IP 发起请求,结果卡在 SYN_WAIT
• 测试环境 OK,生产环境因 DNS 响应差异突然失败
- 别假设
net.LookupIP返回的第一个是 IPv4;它甚至可能全为 IPv6 - 别在没验证连通性前直接用返回值构造
net.Conn - 如果业务强依赖 IPv4(如某些嵌入式设备只支持 AF_INET),必须显式过滤
如何安全提取 IPv4 或 IPv6 地址
用 ip.To4() 判断是否为 IPv4,用 ip.To16() != nil && ip.To4() == nil 判断是否为 IPv6。注意:IPv4-mapped IPv6 地址(如 ::ffff:192.168.1.1)会被 To4() 识别为 IPv4,通常这不是你想要的。
立即学习“go语言免费学习笔记(深入)”;
使用场景:建立 TCP 连接前预筛选地址、健康检查目标列表生成、日志中标准化输出 IP 类型
ips, err := net.LookupIP("example.com")
if err != nil {
return err
}
var ipv4s, ipv6s []net.IP
for _, ip := range ips {
if ip4 := ip.To4(); ip4 != nil {
ipv4s = append(ipv4s, ip4)
} else if ip.To16() != nil {
ipv6s = append(ipv6s, ip)
}
}
-
net.IP.To4()返回nil不代表是 IPv6,可能是非法地址(如全零) - 不要用
strings.Contains(ip.String(), ":")判 IPv6 —— IPv4-mapped 地址也含冒号 - 若需优先 IPv4,就从
ipv4s取;若需双栈 fallback,按顺序尝试两个切片
想“自动选可用地址”?别信封装好的“智能解析”库
很多第三方包声称“自动优选最快/可达地址”,实际只是对 net.LookupIP 结果做 ping 或 connect timeout 检测。问题在于:
• ICMP 可能被防火墙拦截(ping 失败 ≠ 不可达)
• TCP connect 测试会真实建链,高并发下易触发限速或被远端拒绝
• 本地路由表变化(如 WiFi 切换)会让前一秒“可用”的地址下一秒失效
更务实的做法是:让调用方控制策略,而不是让解析函数越权决定
- 用
net.Dialer的KeepAlive: 0+Timeout控制单次连接行为,而非提前筛 IP - 如果真要探测,用
net.Dialer.DialContext对每个候选 IP 异步试连,设短超时(≤ 300ms) - 避免在
init()或全局变量初始化时做 DNS 查询 —— 此时网络栈可能未就绪
net.Resolver 的 PreferGo 和 StrictErrors 怎么影响结果
默认 Resolver 走系统 C 库,设 PreferGo: true 会启用 Go 自研 DNS 解析器(纯 Go 实现),好处是:不依赖 libc、可控制超时、支持 EDNS;坏处是:不读 /etc/nsswitch.conf、忽略 hosts 文件、不支持 mDNS。
StrictErrors: true 会让部分非致命 DNS 错误(如 SERVFAIL)直接返回 error,而不是继续尝试备用 nameserver —— 这在调试时有用,但线上建议关掉。
- 设
PreferGo: true后,net.LookupIP对 AAAA 记录的处理更稳定,但依然不保证顺序 - Go 解析器默认并发查 A 和 AAAA,所以返回顺序仍是不确定的
- 若应用部署在容器里且
/etc/resolv.conf配了多个 nameserver,PreferGo会轮询它们,而系统解析器可能只用第一个
真正麻烦的从来不是怎么拿到 IPv4 或 IPv6,而是同一个域名在不同机器、不同时刻、不同 glibc 版本下,net.LookupIP 返回的切片长度和顺序都可能不同。把“选择逻辑”下沉到连接建立那一刻,比在解析阶段强行归一更可靠。










