學學 Go1-18 新 IP 包的設計思路

大家好,我是 polarisxu。

Go 1.18 標準庫新增了一個包:net/netip,大部分人可能用不上這個包,但這個包的設計思路以及和現有標準庫 IP 的比較值得學習。

01 標準庫 net.IP 的問題

前 Go Team 成員之一 Brad Fitzpatrick 加入 Tailscale[1] 後,經常需要操作 IP 地址。因爲使用 Go 語言實現的,自然會使用過標準庫的 net.IP 和 net.IPNet 等類型。但他們認爲標準庫的相關類型有很多問題,所以他們自己寫了一個包:https://github.com/inetaf/netaddr。

早在 2017 年 1 月,Brad Fitzpatrick 就提了 issue,認爲 net.IP 的設計存在問題:https://github.com/golang/go/issues/18804,那時他還在 Go Team。

具體來說,net.IP 存在如下幾個問題:

但爲了兼容性,以上這些問題沒法通過改進 net.IP 類型解決。於是纔有了 Brad Fitzpatrick 上面開發的包。該包已經正式合入 Go1.18 標準庫中,這就是 net/netip 包,這裏可以查看包文檔:https://pkg.go.dev/net/netip@master。

02 net/netip 包設計思路

新的 netip 包定義了一個 IP 地址(Addr)類型,它是一個小值類型。基於該 Addr 類型,該包還定義了 AddrPort(一個 IP 地址和一個端口)和 Prefix(一個 IP 地址和一個位長前綴)。

與 net.IP 類型相比,netip 包的 Addr 類型佔用更少的內存(24 byte),不可變(immutable),並且具有可比性(支持 == 並作爲 map 鍵)。(本文基於 64 位機器講解)

該包的具體 API 等信息可以查看文檔,這裏着重講解 netip 的設計思路。(來自 Brad Fitzpatrick 的文章)

net.IP 類型的特性:

net.IP 特性

基於此,netip 包的演進過程中,有幾種設計。

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
}

這種結構相比 net.IP 結果:

wgcfg 對比

可見還存在幾個問題:1)無法區分 IPv4 和 IPv6;2)不支持 IPv6 zone。而不透明可以通過將字段 Addr 改爲 addr 解決。

2)netaddr.IP,查看具體代碼 [5]。

不知道大家是否知道,Go 中的 interface 是可比較的(即可通過 == 比較和用作 map 的鍵,不過如果接口的底層值是不可比的,則運行時會 panic)。利用這一點,設計了 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
}

該結構的對比:

netaddr.IP

這種結構存在的問題:不夠小(20-23 byte),不是 Allocation free。

因此繼續優化。

3)allocation-free 24 字節表示

爲什麼定爲 24 字節?Go 標準庫中 net.IP 的 Slice Header 大小是 24 字節,而 Go 中 Slice 很常見。time.Time 類型的大小目前也是 24 字節。所以,Go 編譯器肯定能夠很好的處理 24 字節值類型。所以,tailscale 團隊定了目標,要求表示 IP 的類型不超過 24 字節。

由於 IPv6 地址已經佔去 16 個字節,因此剩下 8 字節用於編碼以下內容:

此外,還需要能比較。

剩下的內容只能佔 8 字節,因此沒法使用 interface{}(它佔用 16 字節),字符串也不行(16 字節)。

可以嘗試採用了 bit-packing 的方式:

type IP struct {
   addr          [16]byte
   zoneAndFamily uint64
}

將地址族和 IPv6 zone 打包(packing)進 zoneAndFamily 字段中(8 字節)。不過這種方式編碼不是太方便,可能還會有一些問題。

最後採用了指針的方式:

type IP struct {
    addr [16]byte
    z    *intern.Value // zone and family
}

具體的過程分析可以參考 https://tailscale.com/blog/netaddr-new-ip-type-for-go/。

這樣可以定義三個哨兵:

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
)

這接近最終實現。不過,基於此有進一步的優化,感興趣的可以閱讀上面參考文章以及 Go1.18 的 net/netip 實現。

allocation-free

03 總結

這個包你可能用不到,不過標準庫中之前的 IP 實現的問題,以及新 IP 類型的設計思路還是值得認真看一下的。對其中更多細節感興趣的,可以認真研讀這篇文章:https://tailscale.com/blog/netaddr-new-ip-type-for-go/。

參考資料

[1]

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

堅持輸出技術(包括 Go、Rust 等技術)、職場心得和創業感悟!歡迎關注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/VxJvLRTt3zoGRTzAhZTUsQ