登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go net.Resolver.LookupNetIP 怎样只取 IPv4 或 IPv6 结果

来源:17golang原创

时间:2026-11-11 03:57:13 427浏览 收藏

双栈服务里,如果连接器明确只支持 IPv4 或 IPv6,直接调用 LookupNetIP 得到一组地址后再“猜”哪一个能用,往往会把地址族选择和连接失败混在一起。更直接的做法是:查询阶段就把 network 设为 ip4 或 ip6;只有确实需要同时保留两类结果时,才用 netip.Addr.Is4 或 Is6 做二次筛选。

官方文档:https://pkg.go.dev/net#Resolver.LookupNetIP

LookupNetIP(ctx, "ip4", host) 只请求 IPv4 结果,LookupNetIP(ctx, "ip6", host) 只请求 IPv6 结果,"ip" 则保留两种地址。返回值是 []netip.Addr,不要把它和 net.ParseIP 的字符串解析混为一谈。

先用 network 参数决定查询结果

我在给连接器增加 IPv6 兼容时,最先踩到的坑是把 LookupNetIP 当成“先查出所有地址,再由调用方随便过滤”的接口。官方签名已经把地址族选择放在第二个参数里:network 只能使用 ip、ip4 或 ip6。因此,调用方的能力边界如果在查询前已经确定,就应该直接传对应值。

Resolver、LookupNetIP 与 ip、ip4、ip6 地址族结果的静态关系说明图
图1:结构说明图,查看 LookupNetIP 与地址族参数之间的关系。
package main

import (
	"context"
	"fmt"
	"net"
)

func lookupFamily(ctx context.Context, resolver *net.Resolver, network, host string) error {
	// network 只接受 ip、ip4、ip6;由调用方的连接能力决定地址族。
	addrs, err := resolver.LookupNetIP(ctx, network, host)
	if err != nil {
		// DNS 错误和“查询成功但没有目标地址”要分开处理。
		return fmt.Errorf("lookup %s %q: %w", network, host, err)
	}
	if len(addrs) == 0 {
		// 没有可用地址时不要继续拼接目标地址或盲目重试。
		return fmt.Errorf("lookup %s %q returned no address", network, host)
	}

	for _, addr := range addrs {
		// netip.Addr 可直接转换成文本,用于后续拨号或记录日志。
		fmt.Println(addr.String())
	}
	return nil
}

这里的重点不是把 ip4 或 ip6 写成字符串常量,而是让这个选择和后续连接策略保持一致。例如只创建 IPv4 socket 时,不要先用 ip 拿到 IPv6 地址,再等连接阶段报错。查询阶段过滤还能让“没有该地址族”更早暴露出来。

保留双栈查询时再筛选返回值

有些场景需要把两种地址都留在内存里:例如连接器会根据运行环境选择顺序,或者要先记录完整 DNS 结果。这时可以传 ip,然后按 netip.Addr 的方法筛选。Is4 和 Is6 表达的是地址语义,比检查字符串里有没有冒号更稳妥。

双栈 netip.Addr 经过 Is4 和 Is6 筛选后进入连接策略的结构说明图
图2:结构说明图,查看返回地址与 Is4/Is6 筛选结果的关系。
package main

import (
	"context"
	"fmt"
	"net"
	"net/netip"
)

func splitAddresses(ctx context.Context, resolver *net.Resolver, host string) (ipv4, ipv6 []netip.Addr, err error) {
	// ip 保留双栈结果,适合把地址选择延后到连接策略层。
	addrs, err := resolver.LookupNetIP(ctx, "ip", host)
	if err != nil {
		return nil, nil, fmt.Errorf("lookup %q: %w", host, err)
	}

	for _, addr := range addrs {
		// 用地址类型方法筛选,不依赖 String 的展示格式。
		switch {
		case addr.Is4():
			ipv4 = append(ipv4, addr)
		case addr.Is6():
			ipv6 = append(ipv6, addr)
		}
	}
	return ipv4, ipv6, nil
}

二次筛选的代价是调用方要自己处理空切片:只有 IPv4 结果时,IPv6 列表为空是正常情况,不应该被当成 DNS 失败;反过来也一样。若业务必须使用某一地址族,建议把“无结果”转成带有 network 和 host 的错误,让上层决定降级、换地址族还是结束请求。

把地址族选择接到连接逻辑

我最后会把规则压缩成下面三种判断:

目标查询参数适合的后续处理
只要 IPv4ip4直接遍历返回的 IPv4 地址
只要 IPv6ip6直接遍历返回的 IPv6 地址
保留双栈ip用 Is4/Is6 归类,再按策略排序

不要用 net.ParseIP 代替这层选择:它解决的是“把一段文本解析成 IP”,并不负责 DNS 查询,也不会告诉你某个主机名应该返回哪一类地址。LookupNetIP 已经返回 netip.Addr,直接使用其地址族方法即可。

常见追问

ip4 和 ip6 可以写成 tcp4、tcp6 吗?

不可以。LookupNetIP 的 network 参数只接受 ip、ip4、ip6;tcp4、tcp6 是拨号等 TCP API 的网络名,不能直接套到这里。

为什么不总是先查 ip 再过滤?

如果调用方从一开始就只支持一种地址族,直接传 ip4 或 ip6 更清楚,也能把地址族约束放在查询入口;只有需要保留双栈候选或自定义排序时,才值得延后筛选。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>