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

Go net.IP.Mask 怎样把地址归一到网段

来源:17golang原创

时间:2026-11-06 00:51:42 111浏览 收藏

在访问控制、路由表或连接统计里,经常需要把 192.0.2.37 这样的主机地址归一成它所属的网段地址。Go 标准库的 net.IP.Mask 正好做这件事:它把 IP 地址和 net.IPMask 按字节做按位与,掩码为 0 的主机位会被清零,返回类似 192.0.2.0 或 2001:db8:: 的网段地址。

最容易出错的地方不是调用方法,而是掩码长度。IPv4 常用 4 字节掩码,IPv6 使用 16 字节掩码;长度不匹配时结果是 nil。如果输入本来来自 CIDR,优先使用 net.ParseCIDR 返回的 IPNet.IP 和 IPNet.Mask,不要再次手写一套长度判断。

官方资料:https://pkg.go.dev/net#IP.Mask

Mask 做的事情就是清零主机位

IPMask 可以看作和 IP 地址等长的位掩码。前缀部分是 1,主机部分是 0;对每个字节执行按位与后,前缀位保持不变,主机位变成 0。因此,Mask 的结果是地址归一化结果,而不是“这个地址是否属于某个网段”的判断。

IPv4 和 IPv6 地址与 IPMask 按位与得到网段地址的静态关系图
图1:IP 地址与掩码按位与后得到网段地址的结构示意图,不是运行截图。

下面的例子把 IPv4 和 IPv6 放在一起,重点看 CIDRMask 的第二个参数:IPv4 是 32 位,IPv6 是 128 位。代码中的注释说明了掩码的用途和结果判断。

package main

import (
	"fmt"
	"net"
)

func main() {
	// /24 只保留 IPv4 的前 24 位,最后一个字节作为主机位被清零。
	ipv4 := net.ParseIP("192.0.2.37")
	ipv4Mask := net.CIDRMask(24, 32)
	fmt.Println(ipv4.Mask(ipv4Mask)) // 192.0.2.0

	// /32 只保留 IPv6 的前 32 位,后面的 96 位被清零。
	ipv6 := net.ParseIP("2001:db8:a0b:12f0::1")
	ipv6Mask := net.CIDRMask(32, 128)
	fmt.Println(ipv6.Mask(ipv6Mask)) // 2001:db8::
}

这里没有修改 ipv4 或 ipv6 本身,Mask 会返回新的 net.IP 切片。需要作为 map 键或日志字段保存时,可以继续调用 String 得到稳定的文本形式;如果要保留二进制形式,则应注意返回切片的生命周期和复制策略。

IPv4 和 IPv6 的掩码长度要先对齐

net.ParseIP 返回的是 net.IP,而 Go 的 IPv4 语义可能以 16 字节的 IPv4-mapped IPv6 形式出现。标准库的 Mask 对常见的 IPv4 映射情况做了兼容处理,但不能把“能解析成 IP”和“掩码一定匹配”混为一谈。

最直接的规则有三条:

  • IPv4 地址配 net.CIDRMask(prefix, 32),例如 /24。
  • IPv6 地址配 net.CIDRMask(prefix, 128),例如 /64。
  • 如果手写 net.IPMask,必须确认它最终与地址的有效表示长度兼容。

不要把 IPv4 的 /24 直接理解成“掩码一定是 16 字节”,也不要把 IPv6 的前缀长度传给 32 位掩码。CIDRMask 的位宽参数表达的是地址族的总位数,不是当前前缀长度。

ParseCIDR 已经给你准备好网段地址

如果输入格式本身是 地址/前缀,使用 net.ParseCIDR 通常更稳。它返回原始 IP 和一个 *net.IPNet;其中 IPNet.IP 已经是按掩码归一化后的网络地址,IPNet.Mask 是与地址族匹配的掩码。

ParseCIDR、IPNet.IP、IPNet.Mask 与 IPv4 IPv6 表示长度边界结构图
图2:ParseCIDR 返回对象与 Mask 输入边界的结构示意图,不是运行截图。
package main

import (
	"fmt"
	"net"
)

func main() {
	// ParseCIDR 同时返回原始主机地址和已经归一化的 IPNet。
	addr, network, err := net.ParseCIDR("192.0.2.37/24")
	if err != nil {
		// 输入不是合法 CIDR 时立即停止,避免继续使用空结果。
		panic(err)
	}

	fmt.Println("原始地址:", addr)            // 192.0.2.37
	fmt.Println("网段地址:", network.IP)      // 192.0.2.0
	fmt.Println("掩码:", network.Mask.String()) // ffffff00

	// 需要再次归一化时,使用同一个 IPNet 的掩码保持地址族一致。
	fmt.Println("再次归一化:", addr.Mask(network.Mask)) // 192.0.2.0
}

这个场景里,network.IP 已经是可用于网段身份的地址。再次调用 addr.Mask(network.Mask) 主要适合演示或封装统一逻辑;业务代码不必为了得到同一个结果重复计算。

返回 nil 时先查掩码和地址是否匹配

Mask 的返回类型仍然是 net.IP,所以不能只打印结果就忽略失败。源码语义很明确:当处理后的 IP 长度和掩码长度不相等时,返回 nil;匹配时才逐字节分配结果并执行按位与。

package main

import (
	"fmt"
	"net"
)

func normalize(ip net.IP, mask net.IPMask) (net.IP, error) {
	// Mask 返回 nil 表示地址与掩码长度不兼容,不能把它当成 0.0.0.0。
	network := ip.Mask(mask)
	if network == nil {
		return nil, fmt.Errorf("IP 与掩码长度不匹配: ip=%d mask=%d", len(ip), len(mask))
	}
	return network, nil
}

func main() {
	// 这里故意用 IPv4 地址配 16 字节全零掩码,演示失败分支。
	ip := net.ParseIP("192.0.2.37").To4()
	wrongMask := net.IPMask(make([]byte, net.IPv6len))

	if _, err := normalize(ip, wrongMask); err != nil {
		// 记录长度比直接记录 nil 更容易定位地址族或输入转换问题。
		fmt.Println(err)
	}
}

如果你的输入是 ParseIP(...).To4() 得到的 4 字节 IPv4,就使用 32 位掩码;如果保留 IPv6 表示,就使用 128 位掩码。不要为了“让调用成功”盲目把掩码截断,先确认你的业务需要哪一种地址表示。

把归一化地址用于网段键,但别替代包含判断

一个常见用法是把同一网段内的地址归一后生成统计键,例如 network.String() 加上前缀长度。这样 192.0.2.1/24 和 192.0.2.37/24 会落在同一个网段键下。

但“得到网段地址”和“判断某个 IP 是否属于网段”是两件事。前者调用 Mask;后者若已经有 *net.IPNet,应使用它的 Contains 方法,并保持输入地址族一致。不要把两个地址分别 Mask 后相等,就当成所有边界条件都已经被检查。

package main

import (
	"fmt"
	"net"
)

func main() {
	// 解析 CIDR 后,IPNet.IP 适合作为网段身份,Contains 负责归属判断。
	_, network, err := net.ParseCIDR("198.51.100.0/24")
	if err != nil {
		// 解析失败时不创建半成品网段键。
		panic(err)
	}

	client := net.ParseIP("198.51.100.42")
	key := network.IP.String() + "/" + fmt.Sprint(network.Mask.Size())
	fmt.Println("网段键:", key)
	fmt.Println("是否属于网段:", network.Contains(client))
}

示例中的键只表达“这个网段本身”,归属判断仍由 Contains 完成。若你的系统同时接收 IPv4、IPv6 和 IPv4-mapped IPv6,需要在进入这层逻辑前统一地址表示,否则同一个业务地址可能出现不同文本键。

几个容易混淆的细节

  • Mask 不是 CIDR 解析器:它只接收已经准备好的 IPMask,前缀长度应由 CIDRMask 或 ParseCIDR 提供。
  • nil 不代表全零地址:它表示长度不兼容或无法完成按位与;需要显式判断后再使用。
  • IPv4/IPv6 先统一再比较:地址族和字节长度必须有明确约定,不能只看字符串里有没有冒号。
  • 需要归属判断就用 Contains:归一化地址适合做网段身份,不能替代网络对象的成员判断。

相关问题

net.CIDRMask 的第二个参数为什么是 32 或 128?

它表示地址族的总位数:IPv4 是 32 位,IPv6 是 128 位。第一个参数才是前缀中连续 1 的数量。

ParseCIDR 返回的 IP 和 IPNet.IP 有什么区别?

前者保留输入中的主机地址,后者是按掩码清零主机位后的网络地址。需要网段身份时通常使用后者。

为什么 Mask 结果打印成了 nil?

优先检查 IP 的实际字节长度与掩码长度,再检查是否把 IPv4 的 32 位掩码误用于 IPv6,或在 To4 前后混用了两种表示。

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