net.ipnet.contains 不适合高频批量匹配,因其每次调用需重复计算掩码、按位与及比对,无缓存无索引,ipv6 下更慢且不支持最长前缀匹配;高性能场景应选用 trie(如 bart)或手写位运算(限 ipv4 整字节掩码)。

直接用 net.IPNet.Contains 能解决小规模匹配,但查 10 万条 CIDR 时性能会断崖下跌;真要高性能,得换 trie 结构或预处理位运算,不能靠循环遍历。
为什么 net.IPNet.Contains 不适合高频批量匹配
每次调用 Contains 都要重新计算掩码、做按位与、再比对,底层是纯 CPU 运算,无缓存、无索引。对单次判断没问题,但若在 HTTP 中间件里每请求都扫一遍几百条网段,CPU 就明显打满。
- IPv6 下更重:
net.IP是 16 字节 slice,每次比较要跑 16 字节的循环 - 无法利用前缀共性:比如
10.0.0.0/8和10.1.0.0/16其实共享高位,但Contains每次都从头算 - 不支持最长前缀匹配(LPM):你给它
192.168.1.5/32,它只能告诉你“在不在某网段”,没法告诉你“最精确匹配的是哪个”
用 bart 实现 O(log n) 查找和并发安全
bart 是目前 Go 生态里最轻量又可靠的多比特 trie 实现,固定步长 8 位,对 IPv4/IPv6 都做了优化,读操作完全无锁。
- 插入前必须用
netip.MustParsePrefix,别传字符串如"192.168.1.1/24"——bart不接受非法掩码,/33或/0会 panic - 查单个 IP 用
t.Lookup(netip.MustParseAddr("192.168.1.100")),返回 payload 和是否命中 - 要最长前缀匹配(比如 ACL 规则优先级),必须用
t.LookupPrefixLPM,它返回匹配的netip.Prefix而非原始插入键 - 内存占用约 10–15 字节/CIDR(IPv4),10 万条大概 1.2 MB,远低于 map[string]struct{}
手写位运算匹配:只适用于 IPv4 + 固定掩码场景
如果你只处理 IPv4 且掩码全是 /24、/16 这类整字节对齐的,可跳过依赖,用 binary.BigEndian.Uint32 手算:
func inCIDR(ipStr, cidrStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
_, network, err := net.ParseCIDR(cidrStr)
if err != nil {
return false
}
mask := network.Mask
ip4 := ip.To4()
if ip4 == nil {
return false
}
// 提取网络地址的整数表示
networkIP := binary.BigEndian.Uint32(network.IP.To4())
ipNum := binary.BigEndian.Uint32(ip4)
return (ipNum & uint32(mask)) == networkIP
}
- 关键点:必须用
ip.To4()判定是否为 IPv4,否则binary.BigEndian.Uint32对 IPv6 会 panic - 不要自己拼掩码字符串(如
"255.255.255.0"),network.Mask已经是正确字节序 - 这个函数不能处理
::1或2001:db8::/32,IPv6 必须走bart或netip原生方法
容易被忽略的边界:CIDR 归一化与 zone ID
用户输入的 "192.168.1.5/24" 不是合法 CIDR——网络地址必须是归一化的,正确应为 "192.168.1.0/24"。而 net.ParseCIDR 会自动修正,但 netip.ParsePrefix 不会,它直接报错。
- 永远优先用
net.ParseCIDR解析用户输入,再用.IP和.Mask构造netip.Prefix - 带 zone ID 的 IPv6(如
"fe80::1%lo0")不能直接丢进netip.ParseAddr,得先用strings.Split(ipStr, "%")截掉后缀 -
bart插入的 key 是netip.Prefix,不是字符串;如果反复插入同一网段,不会去重,得业务层自己 dedup
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











