0%

go 库学习之 netip

在编写网络相关的程序代码时,我们经常需要处理 IP 地址。当同时需要对 IPv4/IPv6 双栈网络支持时,如何简洁、优雅地表示 IP 地址也是需要些技巧的。

这篇文章将分析 Go 库的 net/netip 包,学习 Go 标准库如何表示和处理 IP 地址/网段。

Addr 类型

netip.Addr 类型可以表示 IPv4 或者 IPv6 地址,它是整个 netip 包的核心类型。它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Addr struct {
addr uint128
z unique.Handle[addrDetail]
}

type uint128 struct {
hi uint64
lo uint64
}

type addrDetail struct {
isV6 bool
zoneV6 string
}

addr 字段是一个 uint128 类型的字段,uint128netip 包内部定义的一个类型,可以用来存储 16 字节长度的整型数。Addr 类型使用该字段来存储 IPv4/IPv6 中的地址数据:它将 16 字节长的地址以大端序的方式存储在该 uint128 类型中,其中最高有效位在 uint128.hi 中、最低有效位在 uint128.lo 中。例如,对于 IPv6 地址 0011:2233:4455:6677:8899:aabb:ccdd:eeff 按照如下方式保存:

  • addr.hi = 0x0011223344556677
  • addr.lo = 0x8899aabbccddeeff

使用两个 uint64 类型的字段而不是 [16]byte 来保存地址数据,使得 IP 地址的绝大多数操作都可以变成 64 位寄存器的算术/位运算,这样比字节操作运算更快。

对于 IPv4 地址,addr 字段保存的是 IPv4-mapped 格式的 IPv6 地址。此时为了快速判断一个 Addr 对象的 IP 地址类型,就需要依赖 z 字段了。它使用了 unique 包来实现 addrDetail 类型值的 intern 化,用于表示该 IP 地址的细节信息,例如区分 IPv4/IPv6 地址、保存 IPv6 地址的 zone 信息。使用 intern 技术来保存地址的详细信息可以节省内存,同时提高比较效率。netip 包预定义了如下 Addr.z 类型的变量,分别表示 Addr 零值、IPv4 地址、IPv6 地址(不含 zone 信息):

1
2
3
4
5
var (
z0 unique.Handle[addrDetail]
z4 = unique.Make(addrDetail{})
z6noz = unique.Make(addrDetail{isV6: true})
)

接下来看 netip 包提供的两个构造 Addr 对象的函数实现,以便加深对 Addr 类型的理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 从 4 字节的 IPv4 地址数据生成 Addr 结构
// AddrFrom4 returns the address of the IPv4 address given by the bytes in addr.
func AddrFrom4(addr [4]byte) Addr {
return Addr{
// 以 IPv4-mapped 格式保存地址数据,即 10 字节 0x0 + 2 字节 0xFF + 4 字节的 IPv4 地址,但是 z 字段仍然为 z4
addr: uint128{0, 0xffff00000000 | uint64(addr[0])<<24 | uint64(addr[1])<<16 | uint64(addr[2])<<8 | uint64(addr[3])},
z: z4,
}
}

// 从 16 字节的 IPv6 地址数据生成 Addr 结构
func AddrFrom16(addr [16]byte) Addr {
return Addr{
addr: uint128{
byteorder.BeUint64(addr[:8]),
byteorder.BeUint64(addr[8:]),
},
z: z6noz,
}
}

可以看到,AddrFrom4/AddrFrom16 函数以大端字节序的方式将代表 IP 地址的字节切片保存到到 uint128 中,同时根据地址类型的不同,设置 Addr.z 字段的值。

接下来简单列举与 netip.Addr 相关的函数及方法。

Addr 构造相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 从 4 字节的 IPv4 地址数据生成 Addr 对象
func AddrFrom4(addr [4]byte) Addr

// 从 16 字节的 IPv6 地址数据生成 Addr 对象
func AddrFrom16(addr [16]byte) Addr

// 从字节切片生成 Addr 对象,字节切片长度必须是 4 或 16,否则无效
func AddrFromSlice(slice []byte) (ip Addr, ok bool)

// 解析字符串格式的 IP 地址,返回 Addr 对象。如果解析失败,则返回 Addr 零值以及错误
// 该函数可以正确解析形如 "192.0.2.1"、"2001:db8::68"、"fe80::1cc0:3e8c:119f:c2e1%ens18"(携带 zone 标识的 IPv6 地址)
// 该函数内部实现就是根据字符串中遇到的第一个 `.` 或 `:` 符号来判断待解析的字符串是 IPv4 还是 IPv6 地址,然后再根据对应的规则去解析地址中的各个字节
// 对于 IPv4-mapped 格式的 IPv6 地址,例如 ::FFFF:192.168.1.1 会被认为是 IPv6 地址
func ParseAddr(s string) (Addr, error)

// 对 ParseAddr 函数的封装,如果解析失败,直接 panic
func MustParseAddr(s string) Addr

Addr 判断相关函数/方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 是否是一个有效的 Addr 对象,即不是 Addr 对象的零值
// 注意 0.0.0.0 和 `::` 都是有效的 Addr,因为它们不是 Addr 的零值
func (ip Addr) IsValid() bool { return ip.z != z0 }

// 返回该 IP 地址的位长,返回值可以是 0(Addr 零值)、32(IPv4 地址)或 128(IPv6 地址)
func (ip Addr) BitLen() int {
switch ip.z {
case z0:
return 0
case z4:
return 32
}
return 128
}

// 判断是否是 IPv4 地址,直接判断 Addr.z 字段是否等于 z4 即可
func (ip Addr) Is4() bool {
return ip.z == z4
}

// 判断是否是 IPv6 地址
func (ip Addr) Is6() bool {
return ip.z != z0 && ip.z != z4
}

// 是否是 IPv4-mapped 格式的 IPv6 地址,它必须首先是个 IPv6 地址,然后前十二字节还需要满足 ::FFFF 要求
func (ip Addr) Is4In6() bool {
// 首先该地址是 IPv6 地址,必须满足要求
return ip.Is6() && ip.addr.hi == 0 && ip.addr.lo>>32 == 0xffff
}

尤其需要注意 Is4()/Is6()/Is4In6() 的判断,虽然在 Addr 中始终都是以 IPv4inIPv6 格式来存储 IPv4 地址数据,但是对于 IPv4 地址,其 z 字段始终都是 z4。而对于真正的 IPv4-mapped 的 IPv6 地址,其 z 字段则不是 z4。以下例子说明了这个区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"net/netip"
)

func main() {
a4 := netip.MustParseAddr("192.168.1.0")
a46 := netip.MustParseAddr("::FFFF:192.168.1.0")
a6 := netip.MustParseAddr("::FFFF:192:168:1:0")

// true false false
fmt.Println(a4.Is4(), a4.Is6(), a4.Is4In6())
// false true true
fmt.Println(a46.Is4(), a46.Is6(), a46.Is4In6())
// false true false
fmt.Println(a6.Is4(), a6.Is6(), a6.Is4In6())
}

以下函数则用来判断某个 Addr 是否属于某类特殊地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 是否是链路本地地址
// 对于 IPv4,为 169.254.0.0/16
// 对于 IPv6,为 fe80::/10
func (ip Addr) IsLinkLocalUnicast() bool

// 是否是环回地址
// 对于 IPv4,为 127.0.0.0/8
// 对于 IPv6,为 ::1/128
func (ip Addr) IsLoopback() bool

// 是否是组播地址
// 对于 IPv4,为 224.0.0.0/4
// 对于 IPv6,为 0xFF00::/8
func (ip Addr) IsMulticast() bool

// 是否是接口本地多播地址
// https://datatracker.ietf.org/doc/html/rfc4291#section-2.7.1
// FF01::/16
func (ip Addr) IsInterfaceLocalMulticast() bool

// 是否是链路本地多播地址
// 对于 IPv4,为 224.0.0.0/24
// 对于 IPv6,为 0xFF02::/16
func (ip Addr) IsLinkLocalMulticast() bool

// 是否是全球单播地址
func (ip Addr) IsGlobalUnicast() bool

// 是否是私有地址
// 对于 IPv4,为 10.0.0.0/8、172.16.0.0/12 和 192.168.0.0/16
// 对于 IPv6,为 fc00::/7
func (ip Addr) IsPrivate() bool

// 是否是未指定地址
// 对于 IPv4,为 0.0.0.0
// 对于 IPv6,为 ::
// func (ip Addr) IsUnspecified() bool

另外,netip 库也提供了以下函数,直接生成某些特殊地址:

1
2
3
4
5
func IPv4Unspecified() Addr
func IPv6Unspecified() Addr
func IPv6LinkLocalAllNodes() Addr
func IPv6LinkLocalAllRouters() Addr
func IPv6Loopback() Addr

Addr 其他函数/方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 返回该 IP 地址的 zone 信息,仅对 IPv6 地址有意义。对于 Addr 零值或者 IPv4 地址,都是返回空字符串
func (ip Addr) Zone() string

// 为 Addr 设置指定的 zone 信息,仅对 IPv6 地址有意义
func (ip Addr) WithZone(zone string) Addr

// Addr 的比较函数,如果 ip < ip2,返回 -1;如果 ip == ip2,返回 0;如果 ip > ip2,返回 1
// 按照位长、地址数据、zone 信息的优先级依次比较
func (ip Addr) Compare(ip2 Addr) int

// Addr 的小于判断函数
func (ip Addr) Less(ip2 Addr) bool { return ip.Compare(ip2) == -1 }

// 将 IPv4-mapped 的 IPv6 地址 unmap 成 IPv4 地址
func (ip Addr) Unmap() Addr

// 将 IP 地址转换为 16 字节的数组
func (ip Addr) As16() (a16 [16]byte)

// 将 IP 地址转换为 4 字节的数组,只对 IPv4 地址以及 IPv4-mapped 的 IPv6 地址有意义
func (ip Addr) As4() (a4 [4]byte)

// 将 IP 地址转换为字节切片,如果是 Addr 零值,返回 nil
// 对于 IPv4 地址,返回 4 字节的切片,对于 IPv6 地址,返回 16 字节的切片
func (ip Addr) AsSlice() []byte

// 返回指定 IP 地址的下一个地址,如果已经是最后一个地址,则返回 Addr 零值
func (ip Addr) Next() Addr

// 返回指定 IP 地址的上一个地址,如果已经是全 0 地址,则返回 Addr 零值
func (ip Addr) Prev() Addr

// 实现了 fmt.Stringer 接口,返回 IP 地址的字符串表示,对于 Addr 零值,返回 "invalid IP"
func (ip Addr) String() string

// 将 IP 地址转换为字符字符串,并添加到指定的字节切片 b 中
func (ip Addr) AppendTo(b []byte) []byte

// 同样将 IP 地址转换为字符串,但是对于 IPv6 地址会对每个分段正确填充前导 0,以及对于 `::` 缩写法也会进行扩展
// 例如对于地址 "2001:db8::1",会表示为 "2001:0db8:0000:0000:0000:0000:0000:0001" 形式
func (ip Addr) StringExpanded() string

// 分别实现了 encoding.TextMarshaler 和 encoding.TextUnmarshaler 接口,用于实现 Addr 与文本字节串之间的转换
func (ip Addr) MarshalText() ([]byte, error)
func (ip *Addr) UnmarshalText(text []byte) error

// 分别实现了 encoding.BinaryMarshaler 和 encoding.BinaryUnmarshaler 接口,用于实现 Addr 与二进制字节串之间的转换
func (ip Addr) MarshalBinary() ([]byte, error)
func (ip *Addr) UnmarshalBinary(b []byte) error

AddrPort 类型

AddrPort 类型表示 IP 地址和端口的组合,它的定义如下:

1
2
3
4
type AddrPort struct {
ip Addr
port uint16
}

AddrPort 类型的定义非常简单,使用 Addr 表示 IP 地址,使用 uint16 表示端口。下面再简单列举 AddrPort 提供的相关函数及方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 基于 ip 和 port 生成 AddrPort 对象
func AddrPortFrom(ip Addr, port uint16) AddrPort
// 解析 "IP:Port" 或 "[IP]:Port" 格式的字符串,生成 AddrPort 对象
// IPv4 类型的地址需要使用 "IP:Port" 字符串格式
// IPv6 类型的地址需要使用 "[IP]:Port" 字符串格式
func ParseAddrPort(s string) (AddrPort, error)
// 对 ParseAddrPort 的封装,如果解析失败,直接 panic
func MustParseAddrPort(s string) AddrPort

// 返回 AddrPort 中的 IP 地址和端口
func (p AddrPort) Addr() Addr { return p.ip }
func (p AddrPort) Port() uint16 { return p.port }

// 是否是 AddrPort 类型的有效值,只要 IP 地址不是零值即可
func (p AddrPort) IsValid() bool { return p.ip.IsValid() }

// AddrPort 的比较函数,按照 IP 地址、端口的顺序进行比较
func (p AddrPort) Compare(p2 AddrPort) int

// AddrPort 实现了 fmt.Stringer 接口,返回 AddrPort 的字符串表示
func (p AddrPort) String() string

// 将 AddrPort 转换为字符字符串形式,并添加到指定的字节切片 b 中
func (p AddrPort) AppendTo(b []byte) []byte

// AddrPort 实现了 encoding.TextMarshaler 和 encoding.TextUnmarshaler 接口,用于实现 AddrPort 与文本字节串之间的转换
func (p AddrPort) MarshalText() ([]byte, error)
func (p *AddrPort) UnmarshalText(text []byte) error

// AddrPort 实现了 encoding.BinaryMarshaler 和 encoding.BinaryUnmarshaler 接口,用于实现 AddrPort 与二进制字节串之间的转换。注意其中的 Port 是以小端序保存的
func (p AddrPort) MarshalBinary() ([]byte, error)
func (p *AddrPort) UnmarshalBinary(b []byte) error

Prefix 类型

与网路地址息息相关的一个概念是网段,用于表示一个网络。netip 包定义了 Prefix 类型来表示一个网段,它的定义如下:

1
2
3
4
5
6
7
8
9
10
type Prefix struct {
// 该 IP 网段的 IP 地址
ip Addr

// 该网段的掩码长度 + 1
// 因为 0 是有效的掩码长度,为了将其与 0 值本身进行区分,这里存储为掩码长度 + 1
// 这样当 bitsPlusOne 为 0 时,代表是 Prefix 的零值
// 当 bitsPlusOne 为 1 时,代表掩码长度为 0
bitsPlusOne uint8
}

注意 Prefix 中的 ip 可以是任意的 IP 地址,并不一定要真的是一个网络地址(即主机位全为 0 的 IP 地址)。而且 Prefix 中的 ip 总是没有 zone 信息的。

下面同样列举与 Prefix 类型相关的函数及方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 基于指定的 IP 地址和掩码长度生成 Prefix,注意生成的 Prefix 只是简单地保存原始的 IP 地址,并不会对 IP 地址进行位掩码运算
func PrefixFrom(ip Addr, bits int) Prefix
// Addr 类型提供的一个方法,用于对指定的 IP 地址进行位掩码运算,生成一个 Prefix 对象。此时 Prefix 对象中保存的 IP 地址就是一个掩码运算后的值(即一个网络地址,主机位全为 0)
func (ip Addr) Prefix(b int) (Prefix, error)
// 解析 CIDR 格式字符串,例如 `192.168.1.0/24` `2001:db8::/32`
// 该函数生成的 Prefix 对象中所保存的 ip 地址也不是掩码后的值
func ParsePrefix(s string) (Prefix, error)
// 对 ParsePrefix 的包装,解析失败直接 panic
func MustParsePrefix(s string) Prefix


// 返回 Prefix 中保存的 IP 地址
func (p Prefix) Addr() Addr { return p.ip }
// 返回 Prefix 的有效掩码长度
func (p Prefix) Bits() int { return int(p.bitsPlusOne) - 1 }

// 判断是否是一个有效的 Prefix 对象,即非 Prefix 零值
func (p Prefix) IsValid() bool { return p.bitsPlusOne > 0 }

// 判断该 Prefix 表示的网段是否只包含一个 IP 地址,也就是位掩码长度必须是 32(IPv4) 或 128(IPv6)
func (p Prefix) IsSingleIP() bool { return p.IsValid() && p.Bits() == p.ip.BitLen() }

// 对 Prefix 中的 IP 地址进行掩码运算后,重新生成一个 Prefix 对象。此时新生成的 Prefix 对象中中保存的 IP 地址就是一个网络地址
func (p Prefix) Masked() Prefix
// 判断 Prefix 所表示的网段是否包含指定的 ip 地址
func (p Prefix) Contains(ip Addr) bool
// 判断两个 Prefix 是否存在重叠的 IP 地址范围
func (p Prefix) Overlaps(o Prefix) bool

// 将 Prefix 转换为文本字节序列,并添加到字节切片 b 中
func (p Prefix) AppendTo(b []byte) []byte
// Prefix 实现了 encoding.TextMarshaler 和 encoding.TextUnmarshaler 接口,用于实现 Prefix 与文本字节串之间的转换
func (p Prefix) MarshalText() ([]byte, error)
func (p *Prefix) UnmarshalText(text []byte) error
// Prefix 实现了 encoding.BinaryMarshaler 和 encoding.BinaryUnmarshaler 接口,用于实现 Prefix 与二进制字节串之间的转换
func (p Prefix) MarshalBinary() ([]byte, error)
func (p *Prefix) UnmarshalBinary(b []byte) error

// Prefix 实现了 fmt.Stringer 接口,返回 Prefix 的字符串表示
func (p Prefix) String() string

net.IPnet.IPMasknet.IPNet 类型

除了 net/netip 包提供的 Addr 及相关类型外,标准库 net 包中还定义了 IPIPMaskIPNet 类型,分别用于表示 IP 地址、IP 掩码、IP 网段。相关的代码位于 src/net/ip.go 文件中。相比于 netip 包,net 包中的 IP 地址实现更加简单,但是是不可比较的(即不支持 == 或者作为 map 的 key)。而且 net 包的部分实现也依赖于 netip 包。

如下展示了 net 包中这些类型的定义:

1
2
3
4
5
6
7
8
9
10
11
package net

// IP 地址
type IP []byte
// IP 掩码
type IPMask []byte
// IP 网段
type IPNet struct {
IP IP
Mask IPMask
}

可以看到,IP 和 IPMask 都是基于字节切片类型实现的,因此它们都是不可比较类型。无论对于 IPv4 还是 IPv6 地址,内部实现总是使用 16 字节来保存 IP 地址数据。即对于 IPv4 地址,总是保存其 IPv4-mapped 格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var v4InV6Prefix = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}

// 构造一个 IPv4 类型的 IP 地址
func IPv4(a, b, c, d byte) IP {
// 内部总是使用 16 字节来存储 IP 地址
// 即使用 IPv4-mapped 的 IPv6 地址格式来存储该 IPv4 地址
// ::ffff:.a.b.c.d
p := make(IP, IPv6len)
copy(p, v4InV6Prefix)
p[12] = a
p[13] = b
p[14] = c
p[15] = d
return p
}

而 IPMask 则是根据实际的地址类型,分配对应的字节切片长度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const (
IPv4len = 4
IPv6len = 16
)

// 构造一个 IPv4 类型的网络掩码
func IPv4Mask(a, b, c, d byte) IPMask {
// 掩码只使用 4 字节
p := make(IPMask, IPv4len)
p[0] = a
p[1] = b
p[2] = c
p[3] = d
return p
}

在使用 net 包提供的 IP 相关功能时,一定要注意上述实现细节。不能简单的根据字节切片长度来区分是否是 IPv4 还是 IPv6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "net"

func main() {
ip := net.IPv4(1, 1, 1, 1)
m := net.IPv4Mask(1, 1, 1, 1)
// 16, 4
println(len(ip), len(m))

ip = ip.To4()
// 4, 4
println(len(ip), len(m))
}

下面简单列举 net 包提供的 IP 地址/网络相关的函数及方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 根据 4 个字节创建 IPv4 类型的 IP,内部总是使用 IPv4-mapped 格式存储该 IP 地址(16 字节)
func IPv4(a, b, c, d byte) IP
// 将字符串格式解析为 IP 地址。如果解析成功,返回的 IP 地址总是 16 字节长度。但是不支持携带 zone 信息
func ParseIP(s string) IP

// 将 IPv4 地址转换为 4 字节表示
// 如果该 IP 地址已经是 4 字节表示形式,直接返回即可
// 否则如果该 IP 地址是 IPv4-mapped 的 IPv6 地址,直接其中的 IPv4 地址数据即可(最后 4 字节)
// 否则返回 nil
func (ip IP) To4() IP

// 将 IP 地址转换为 16 字节表示形式
// 如果当前 IP 地址是 4 字节表示形式,转换为其对应的 IPv4-mapped 的 IPv6 地址
// 如果当前 IP 地址已经是 16 字节表示形式,直接返回即可
// 否则返回 nil
func (ip IP) To16() IP

// 根据 IP 地址的分类(A 类、B 类、C 类),返回其对应的网络掩码。该函数只对 IPv4 地址有效
func (ip IP) DefaultMask() IPMask

// 对指定的 IP 地址应用 mask 位掩码运算后,得到的 IP 地址
// 如果 mask 是 16 字节长度,但 IP 地址是 4 字节长度,会取 mask 的最后 4 个字节参与运算(前提是 mask 的前 12 个字节全是 0xFF)
// 如果 mask 是 4 字节长度,但 IP 地址是 16 字节长度,且为 IPv4-mapped 的 IPv6 地址,会取地址中的 IPv4 部分(最后 4 个字节)参与运算
func (ip IP) Mask(mask IPMask) IP

// IP 类型实现了 format.Stringer 接口,返回 IP 地址的字符串表示
func (ip IP) String() string
// IP 类型实现了 encoding.TextMarshaler 和 encoding.TextUnmarshaler 接口,用于 IP 类型与文本字节串之间的相关转换
func (ip IP) MarshalText() ([]byte, error)
func (ip *IP) UnmarshalText(text []byte) error

// 判断两个 IP 地址是否相等。IPv4 地址和其 IPv4-mapped 的 IPv6 地址被认为是相等的
func (ip IP) Equal(x IP) bool

// IP 类型也提供了一些判断特殊地址类型的函数,这里就不再列举了
1
2
3
4
5
6
7
8
9
10
// 基于 4 个字节创建 Ipv4 类型的网络掩码。不同于 IPv4 地址,该 IPMask 只占据 4 字节
func IPv4Mask(a, b, c, d byte) IPMask
// 根据掩码中 1 的个数以及掩码的总位数,生成对应的 IPMask。bits 值必须是 32 或 128
func CIDRMask(ones, bits int) IPMask

// 返回 IPMask 中 1 的个数以及掩码的总位数
func (m IPMask) Size() (ones, bits int)

// IPMask 实现了 format.Stringer 接口,返回 IPMask 的字符串表示
func (m IPMask) String() string
1
2
3
4
5
6
7
8
// 对将 CIDR 格式的字符串进行解析
// 如果解析成功,返回值 IP 表示解析到的 IP 地址,而 `IPNet` 中的 IP 地址则是运用了掩码运算后的 IP 地址
func ParseCIDR(s string) (IP, *IPNet, error)
// 判断一个 IP 网络中是否包含指定的 IP 地址
func (n *IPNet) Contains(ip IP) bool

// IPNet 实现了 format.Stringer 接口,返回其字符串表示
func (n *IPNet) String() string

小结

文本介绍了 Go 标准库为 IP 地址/网络等功能提供的基础设施。net/netipnet 包都提供了 IP 地址相关的 API,我们可以根据实际需要选择使用对应的工具。但是需要注意,这些包中提供的地址类型被用于同时表示 IPv4 或 IPv6,而 IPv4-mapped 的 IPv6 地址又使情况更加复杂,因此在使用相关接口时需要额外小心。

Reference