当前位置: 首页 > 科技观察

Go1.18快报:新IP礼包

时间:2023-03-16 22:23:21 科技观察

本文转载自微信公众号「polarisxu」,作者polaris。转载本文请联系polarisxu公众号。大家好,我是polarisxu。Go1.18标准库添加了一个新包:net/netip。大多数人可能不会用到这个包,但是这个包的设计思路和与现有标准库IP的对比是值得学习的。01标准库net.IP的问题GoTeam前成员之一BradFitzpatrick在加入Tailscale[1]后,经常需要操作IP地址。因为是Go语言实现的,自然会用到标准库的net.IP和net.IPNet类型。但是他们觉得标准库中相关类型的问题很多,于是自己写了一个包:https://github.com/inetaf/netaddr。早在2017年1月,BradFitzpatrick就提出了一个issue,认为是net.IP的设计有问题:https://github.com/golang/go/issues/18804,当时他还在GoTeam.具体来说,net.IP存在以下问题:变量。net.IP的底层类型是[]byte,它的定义是:typeIP[]byte,意思是可以随意修改。不可变数据结构更安全、更简单。无与伦比。因为Go中的slice类型是不可比较的,也就是说net.IP不支持==,也不能作为mapkey。有两种IP地址类型,用于基本IPv4或IPv6地址的net.IP和用于支持区域范围的IPv6的net.IPAddr。因为有两种,所以在使用的时候就有一个选择的问题,用哪一种。标准库中存在两种这样的方法:Resolver.LookupIP与Resolver.LookupIPAddr。(有关IPv6区域范围的信息,请参见维基百科:https://en.wikipedia.org/wiki/IPv6_address#Scoped_literal_IPv6_addresses_(with_zone_index。)太大。在Go中,在64位机器上,slicetype占用24个字节,也就是sliceheader。因此,net.IP的大小实际上由两部分组成:一个24字节的切片头和一个4或6字节的IP地址。并且net.IPAddr多了一个字符串类型的Zone字段,占用空间较大。如果不是allocatesfree,会增加GC的工作量。当你调用net.ParseIP或接收UDP数据包时,它会为底层数组分配内存以记录IP地址,然后将指针放入net.IP切片头中。将IP地址解析为字符串时,net.IP无法区分IPv4映射的IPv6地址[2]和IPv4地址。因为net.IP并没有记录原始地址族(addressfamily)。见issue37921[3]是透明类型。因为它的定义是:typeIP[]byte,底层类型是byteslice。这有什么问题吗?我们无法更改IP的基础类型,因为它已经是导出API的一部分。标准库中一个很好的例子就是time.Time类型,它是一个不透明的类型:typeTimestruct{/*unexported*/},也就是没有暴露在里面的东西,让库作者可以修改内容随意,只需要保证导出的API不变即可。事实上,Go1.9只改变了一次time.Time的内部结构,完全不会破坏兼容性。但是为了兼容性,上述问题并不能通过改进net.IP类型来解决。因此,上面由BradFitzpatrick开发的软件包。该包已经正式集成到Go1.18标准库中,即net/netip包,您可以在这里查看包文档:https://pkg.go.dev/net/netip@master。02net/netip包设计思想新的netip包定义了一个IP地址(Addr)类型,是一个小值类型。基于Addr类型,该包还定义了AddrPort(一个IP地址和一个端口)和Prefix(一个IP地址和一个位长前缀)。与net.IP类型相比,netip包的Addr类型占用内存少(24字节),不可变(immutable),具有可比性(支持==并充当映射键)。(本文基于64位机器。)该包的具体API等信息可以查看文档。这里重点讲解netip的设计思想。(摘自BradFitzpatrick的文章)Net.IPTypeFeatures:Net.IPfeatures是基于此的,在netip包的演进中有几种设计。1)wgcfg.IP,具体代码见[4]。//InternallytheaddressisalwaysrepresentedinitsIPv6form.//IPv4addressesusetheIPv4-in-IPv6syntax.typeIPstruct{Addr[16]byte}与net.IP对比结果:wgcfg对比发现还有几个问题:1)不能区分IPv4和IPv6;2)不支持IPv6区域。不透明度可以通过将字段Addr更改为addr来解决。2)netaddr.IP,具体见代码[5]。不知道大家知不知道,Go中的interface是可比较的(可以通过==来比较,作为map的key,但是如果interface的底层值不可比较,runtime会panic)。利用这个,设计了netaddr.IP类型:typeIPstruct{ipImpl}typeipImplinterface{is4()boolis6()boolString()string}typev4Addr[4]bytetypev6Addr[16]bytetypev6AddrZonestruct{v6Addrzonestring}这个结构的比较:netaddr.IP问题结构:不够小(20-23字节),分配不自由。所以继续优化。3)为什么allocation-free24bytes定义为24bytes?Go标准库中net.IP的SliceHeader大小为24字节,Slice在Go中很常见。time.Time类型的大小目前也是24个字节。所以,Go编译器必须能够很好地处理24字节的值类型。所以tailscale团队定下了一个目标,要求IP的类型不能超过24字节。由于IPv6地址已经占用16个字节,因此还剩下8个字节来对以下内容进行编码:地址族(v4、v6或两者都不是,例如IP零值),IPv6区域至少需要2个数字此外,还需要能够比较。剩下的只能占用8个字节,所以不能用interface{}(占用16个字节),也不能用strings(16个字节)。您可以尝试位打包:typeIPstruct{addr[16]bytezoneAndFamilyuint64}将地址族和IPv6区域打包到zoneAndFamily字段(8字节)中。但是,这种编码方式不是很方便,而且可能会出现一些问题。最后采用指针方式:typeIPstruct{addr[16]bytez*intern.Value//zoneandfamily}具体流程分析可以参考https://tailscale.com/blog/netaddr-new-ip-type-for-去/。这允许定义三个哨兵:var(z0*intern.Value//nilforthezerovaluez4=new(intern.Value)//sentinelvaluetomeanIPv4z6noz=new(intern.Value)//sentinelvaluetomeanIPv6withnozone)这接近于最终实现。但是,在此基础上,还有进一步的优化。有兴趣的可以看上面的参考文章和Go1.18的net/netip实现。allocation-free03总结这个包你可能不会用到,但是标准库中以往IP的实现问题,以及新IP类型的设计思路还是值得仔细研究的。如果你对更多细节感兴趣,可以仔细阅读这篇文章:https://tailscale.com/blog/netaddr-new-ip-type-for-go/。References[1]Tailscale:https://tailscale.com/[2]IPv4-mappedIPv6addresses:https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses[3]issue37921:https://github.com/golang/go/issues/37921[4]具体代码:https://github.com/tailscale/wireguard-go/commit/89476f8cb53b7b6e3e67041d204a972b69902565#diff-d6e6f254849cb9119d9aaa21a41ee7f26f499251ce073522bdd89361a316814bR13[5]具体代码:https://github.com/inetaf/netaddr/commit/7f2e8c8409b7c27c5b44192839c8a94fca95aa21#diff-5aea5a23fd374194efa71dd12c8ddf8ede924f1043045520a8283d2490f40f12R27