Go 1.18引入了新库netaddr
来表示IP地址及相关操作。其作者Brad Fitzpatrick专门写了篇blog说明这个库的设计原则和最终实现。
这个实现最主要的特性依赖intern.Value这个库。这里记录一下我对这个库的一些研究和看法
netaddr
的设计原则是希望一个类型可以同时支持IPv4,无区域IPv6以及有区域IPv6,同时希望这个类型是一个值类型,可以使用==
进行正确的比较,且内存占用尽量小。这个要求确实很难。具体的设计过程可以去参考库作者的blog。
最终的实现结果:
其中addr
用来保存实际的IP地址(如果是IPv4,则只使用低32位),z
即作为一个标志位,用来区分IPv4,无区域IPv6及有区域IPv6,同时也用来记录区域信息。由于区域信息可以是任意字符串,正确的实现要求intern.Value
在具有相同内容的字符串时,指向相同的地址。
这里的z
没有使用字符串,我猜是为了尽量压缩IP结构的大小。Go一个字符串会固定占用16byte(内部一个指向[]byte的指针,一个int表是字符串长度),比指针8byte要大一倍。不过使用字符串会让实现更容易理解:
除了比原来的结构多了8byte,也做到了其余的目标。
下面看看intern.Value
是如何在节省8byte情况下,实现同样的功能。根据功能在具有相同内容的字符串时,指向相同的地址
,一个非常直白的实现是这样的:
不考虑并发,这个实现最大的问题是内存泄漏。所有Get返回的指针,都会被values持久引用。要解决内存泄漏问题,需要请出unsafe库。这就很接近intern.Value
的实现:
|
|
valMap
并没有引用Value
,只是用unsafe.Pointer
的方式记录下了Value
的地址。当外部所有对Value
的引用失效后,GC过程会触发finalize
做检查。如果两轮finalize
后Value
还没有被引用过,则从valMap
里删除对应的记录地址。Value
会在再下一次的GC过程中(因为这次没有挂接finalize
)被删除。
如果再加上保护并发的锁,就和intern.Value
的实现差不多了。intern.Value
还考虑了非字符串值的情况。
这里如此麻烦的原因,在于==
只能做一层实例值比较,也不能自定义。考虑到自定义==
带来的问题,这种unsafe的交换大概还能承受。
一个漏洞,如果有外部程序也通过unsafe.Pointer
记录了某个Value
的地址,那么有可能过一段时间后,具有同样内容的Value
地址会变化。
说实话,我不太喜欢intern.Value
的实现。可能底层库真的很缺这8byte的大小吧。