學(xué)學(xué) Go1.18 新 IP 包的設(shè)計(jì)思路
閱讀本文大概需要 10?分鐘。
大家好,我是 polarisxu。
Go 1.18 標(biāo)準(zhǔn)庫(kù)新增了一個(gè)包:net/netip,大部分人可能用不上這個(gè)包,但這個(gè)包的設(shè)計(jì)思路以及和現(xiàn)有標(biāo)準(zhǔn)庫(kù) IP 的比較值得學(xué)習(xí)。
01 標(biāo)準(zhǔn)庫(kù) net.IP 的問題
前 Go Team 成員之一 Brad Fitzpatrick 加入 Tailscale[1] 后,經(jīng)常需要操作 IP 地址。因?yàn)槭褂?Go 語(yǔ)言實(shí)現(xiàn)的,自然會(huì)使用過標(biāo)準(zhǔn)庫(kù)的 net.IP 和 net.IPNet 等類型。但他們認(rèn)為標(biāo)準(zhǔn)庫(kù)的相關(guān)類型有很多問題,所以他們自己寫了一個(gè)包:https://github.com/inetaf/netaddr。
早在 2017 年 1 月,Brad Fitzpatrick 就提了 issue,認(rèn)為 net.IP 的設(shè)計(jì)存在問題:https://github.com/golang/go/issues/18804,那時(shí)他還在 Go Team。
具體來(lái)說(shuō),net.IP 存在如下幾個(gè)問題:
可變的。net.IP 的底層類型是 []byte,它的定義是: type IP []byte,這意味著你可以隨意修改它。不可變數(shù)據(jù)結(jié)構(gòu)更安全、更簡(jiǎn)單。不可比較的。因?yàn)?Go 中 slice 類型是不可比較的,也就是說(shuō) net.IP 不支持 ==,也不能作為 map 的 key。有兩個(gè) IP 地址類型,net.IP 表示基本的 IPv4 或 IPv6 地址,而 net.IPAddr 表示支持 zone scopes 的 IPv6。因?yàn)橛袃蓚€(gè)類型,使用時(shí)就存在選擇問題,到底使用哪個(gè)。標(biāo)準(zhǔn)庫(kù)存在兩個(gè)這樣的方法: Resolver.LookupIPvsResolver.LookupIPAddr。(關(guān)于什么是 IPv6 zone scopes 見維基百科:https://en.wikipedia.org/wiki/IPv6_address#Scoped_literal_IPv6_addresses_(with_zone_index 。)太大。在 Go 中,64 位機(jī)器上,slice 類型占 24 個(gè)字節(jié),這只是 slice header。因此,net.IP 的大小實(shí)際包含兩部分:24 字節(jié)的 slice header 和 4 或 6 字節(jié)的 IP 地址。而 net.IPAddr 更有額外的字符串類型 Zone 字段,占用空間更多。 不是 allocates free 的,會(huì)增加 GC 的工作。當(dāng)你調(diào)用 net.ParseIP 或接收一個(gè) UDP 包時(shí),它為了記錄 IP 地址會(huì)分配底層數(shù)組的內(nèi)存,然后指針放入 net.IP 的 slice header 中。 當(dāng)解析一個(gè)字符串形式的 IP 地址時(shí),net.IP 無(wú)法區(qū)分 IPv4 映射的 IPv6 地址[2]和 IPv4 地址。因?yàn)?net.IP 不會(huì)記錄原始的地址族(address family)。見 issue 37921[3] 它是一種透明類型(transparent type)。因?yàn)樗亩x是: type IP []byte,底層類型是一個(gè)字節(jié)切片。這有什么問題呢?我們沒法改變 IP 的底層類型了,因?yàn)樗呀?jīng)是導(dǎo)出 API 的一部分。標(biāo)準(zhǔn)庫(kù)中一個(gè)很好的例子是 time.Time 類型,它是一個(gè)不透明類型:type Time struct { /* unexported */ },也就是里面有什么沒公開,這樣庫(kù)作者可以隨便修改里面的內(nèi)容,只需要保證導(dǎo)出 API 不變即可。實(shí)際上 Go1.9 就改過一次 time.Time 的內(nèi)部結(jié)構(gòu),完全不會(huì)破壞兼容性。
但為了兼容性,以上這些問題沒法通過改進(jìn) net.IP 類型解決。于是才有了 Brad Fitzpatrick 上面開發(fā)的包。該包已經(jīng)正式合入 Go1.18 標(biāo)準(zhǔn)庫(kù)中,這就是 net/netip 包,這里可以查看包文檔:https://pkg.go.dev/net/netip@master。
02 net/netip 包設(shè)計(jì)思路
新的 netip 包定義了一個(gè) IP 地址(Addr)類型,它是一個(gè)小值類型?;谠?Addr 類型,該包還定義了 AddrPort(一個(gè) IP 地址和一個(gè)端口)和 Prefix(一個(gè) IP 地址和一個(gè)位長(zhǎng)前綴)。
與 net.IP 類型相比,netip 包的 Addr 類型占用更少的內(nèi)存(24 byte),不可變(immutable),并且具有可比性(支持 == 并作為 map 鍵)。(本文基于 64 位機(jī)器講解)
該包的具體 API 等信息可以查看文檔,這里著重講解 netip 的設(shè)計(jì)思路。(來(lái)自 Brad Fitzpatrick 的文章)
net.IP 類型的特性:

基于此,netip 包的演進(jìn)過程中,有幾種設(shè)計(jì)。
1)wgcfg.IP,查看具體代碼[4]。
//?Internally?the?address?is?always?represented?in?its?IPv6?form.
//?IPv4?addresses?use?the?IPv4-in-IPv6?syntax.
type?IP?struct?{
???????Addr?[16]byte
}
這種結(jié)構(gòu)相比 net.IP 結(jié)果:

可見還存在幾個(gè)問題:1)無(wú)法區(qū)分 IPv4 和 IPv6;2)不支持 IPv6 zone。而不透明可以通過將字段 Addr 改為 addr 解決。
2)netaddr.IP,查看具體代碼[5]。
不知道大家是否知道,Go 中的 interface 是可比較的(即可通過 == 比較和用作 map 的鍵,不過如果接口的底層值是不可比的,則運(yùn)行時(shí)會(huì) panic)。利用這一點(diǎn),設(shè)計(jì)了 netaddr.IP 類型:
type?IP?struct?{
?????ipImpl
}
type?ipImpl?interface?{
?????is4()?bool
?????is6()?bool
?????String()?string
}
type?v4Addr?[4]byte
type?v6Addr?[16]byte
type?v6AddrZone?struct?{
??????v6Addr
??????zone?string
}
該結(jié)構(gòu)的對(duì)比:

這種結(jié)構(gòu)存在的問題:不夠?。?0-23 byte),不是 Allocation free。
因此繼續(xù)優(yōu)化。
3)allocation-free 24 字節(jié)表示
為什么定為 24 字節(jié)?Go 標(biāo)準(zhǔn)庫(kù)中 net.IP 的 Slice Header 大小是 24 字節(jié),而 Go 中 Slice 很常見。time.Time 類型的大小目前也是 24 字節(jié)。所以,Go 編譯器肯定能夠很好的處理 24 字節(jié)值類型。所以,tailscale 團(tuán)隊(duì)定了目標(biāo),要求表示 IP 的類型不超過 24 字節(jié)。
由于 IPv6 地址已經(jīng)占去 16 個(gè)字節(jié),因此剩下 8 字節(jié)用于編碼以下內(nèi)容:
地址族(v4、v6 或兩者都不是,例如 IP 零值),至少需要 2 位 IPv6 zone
此外,還需要能比較。
剩下的內(nèi)容只能占 8 字節(jié),因此沒法使用 interface{}(它占用 16 字節(jié)),字符串也不行(16 字節(jié))。
可以嘗試采用了 bit-packing 的方式:
type?IP?struct?{
???addr??????????[16]byte
???zoneAndFamily?uint64
}
將地址族和 IPv6 zone 打包(packing)進(jìn) zoneAndFamily 字段中(8 字節(jié))。不過這種方式編碼不是太方便,可能還會(huì)有一些問題。
最后采用了指針的方式:
type?IP?struct?{
????addr?[16]byte
????z????*intern.Value?//?zone?and?family
}
具體的過程分析可以參考 https://tailscale.com/blog/netaddr-new-ip-type-for-go/。
這樣可以定義三個(gè)哨兵:
var?(
?????z0????*intern.Value????????//?nil?for?the?zero?value
?????z4????=?new(intern.Value)??//?sentinel?value?to?mean?IPv4
?????z6noz?=?new(intern.Value)??//?sentinel?value?to?mean?IPv6?with?no?zone
)
這接近最終實(shí)現(xiàn)。不過,基于此有進(jìn)一步的優(yōu)化,感興趣的可以閱讀上面參考文章以及 Go1.18 的 net/netip 實(shí)現(xiàn)。

03 總結(jié)
這個(gè)包你可能用不到,不過標(biāo)準(zhǔn)庫(kù)中之前的 IP 實(shí)現(xiàn)的問題,以及新 IP 類型的設(shè)計(jì)思路還是值得認(rèn)真看一下的。對(duì)其中更多細(xì)節(jié)感興趣的,可以認(rèn)真研讀這篇文章:https://tailscale.com/blog/netaddr-new-ip-type-for-go/。
參考資料
Tailscale: https://tailscale.com/
[2]IPv4 映射的 IPv6 地址: https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses
[3]issue 37921: 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
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語(yǔ)言并創(chuàng)建了 Go 語(yǔ)言中文網(wǎng)!著有《Go語(yǔ)言編程之旅》、開源圖書《Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)》等。
堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場(chǎng)心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長(zhǎng)!也歡迎加我微信好友交流:gopherstudio
