最可靠的方式是用 net.parsecidr 解析 cidr 得到 *net.ipnet 后调用 contains 方法,它已处理 ipv4/ipv6 对齐、前导零、::1 与 127.0.0.1 等边界情况,百万次查询仅毫秒级。

用 net.IPNet.Contains 做匹配,别手写位运算
最可靠的方式是用标准库 net.ParseCIDR 解析 CIDR 字符串,得到 *net.IPNet,再调用其 Contains 方法判断 IP 是否命中。它内部已处理 IPv4/IPv6 地址对齐、前导零、::1 与 127.0.0.1 等边界情况,性能也足够——实测百万次查询在毫秒级。
常见错误包括:
- 直接传字符串进
Contains:如ipNet.Contains("192.168.1.5")编译不通过; - 用
net.ParseIP结果直接喂给Contains:若解析失败返回nil,调用时 panic; - 用
net.ParseIPMask解析掩码:它只支持点分十进制(如255.0.0.0),无法处理 IPv6 的::/64。
预加载 CIDR 规则,别在 handler 里实时解析
每次请求都调用 net.ParseCIDR 会触发大量小对象分配,GC 压力明显上升。所有黑白名单规则必须在服务启动时一次性加载并缓存。
初始化时注意:
- 检查
net.ParseCIDR的err:像"0.0.0.0/33"这种非法掩码会静默返回(nil, nil),不校验就等于漏掉一条规则; - 剥离端口和方括号:用户 IP 可能来自
X-Forwarded-For,含[::1]:8080或192.168.1.1:12345,需先用net.SplitHostPort拆解,再对 host 部分调用net.ParseIP; - 同一网段重复加载(如
192.168.0.0/16和192.168.1.0/24)会导致逻辑冲突,建议加载阶段做覆盖检测或用github.com/yl2chen/cidranger合并归并。
用 map 查表代替切片遍历,顺序决定策略语义
规则超 100 条后,切片遍历匹配延迟明显上升;而用 map[string]bool 或 map[*net.IPNet]bool(需转 key)可做到 O(1) 查询。
但 *net.IPNet 含 slice 字段,不能直接作 map key。稳妥做法是生成唯一字符串 key:
-
fmt.Sprintf("%s/%d", ipNet.IP, onesCount(ipNet.Mask))—— 更准确,兼容 IPv6; -
ipNet.IP.String() + "/" + ipNet.Mask.String()—— 简单,但 IPv6 掩码字符串可能含前导零,导致 key 不一致。
黑白名单的匹配顺序必须是:先查黑名单,命中即拒绝;未命中再查白名单,未命中则放行。这个“黑名单优先”不是可选项,而是策略前提。
真实 IP 提取必须可信,X-Forwarded-For 不可盲信
直接用 r.RemoteAddr 在有反向代理(Nginx、Cloudflare)时拿到的是代理地址,不是客户端真实 IP。
正确提取逻辑应分层信任:
- 若部署在可信内网且已配置
X-Real-IP,优先取该 header; - 否则取
X-Forwarded-For的第一个非私有地址(需校验是否在10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、::1/128等范围内); - 永远不要把未经清洗的 header 值直接喂给
net.ParseIP,否则遇到逗号分隔多个 IP("1.1.1.1, 2.2.2.2")或带端口格式必 panic。
最易被忽略的一点:黑白名单规则和真实 IP 提取逻辑必须跑在同一信任域下——如果反向代理没开启 set_real_ip_from 或没清理伪造 header,再严谨的 Go 代码也拦不住伪造 IP。











