Go1.18 新特性:引入新的 netip 網路庫

語言: CN / TW / HK

大家好,我是煎魚。

寫這篇文章時是大年初一,原本想說這個月就要釋出 Go1.18 了。但是,好傢伙,Go1.18 beta2 釋出了,官方告知社群 Go1.18 要拖更到 3 月份了,咕咕咕...

如下圖:

所以還是得繼續學習新特性,今天煎魚將結合 Brad Fitzpatrick 寫的《netaddr.IP: a new IP address type for Go》帶大家瞭解 Go1.18 的新網路庫 net/netip 的緣由。

背景

大佬離職

原本 Go 開發團隊中的 Brad Fitzpatrick,在 2010~2020 年都在 Go 團隊工作,在 2021 年起換公司了。

如下推特的訊息:

離職的原因是:做了同樣的東西太久了,有些厭煩,不想陷在一個舒適的困境中。

現在來看是換到了 Tailscale,做 WireGuard 相關工作,要經常與網路庫打交道。

需求誕生

大佬公司寫的 Tailscale,本質上是一個網路應用程式,要與網路打交道,又是用 Go 寫的,就會涉及到標準庫 net: - 在單個 IP 型別上使用 net.IP。 - 網路表示上使用 net.IPNet

示例程式碼:

```go import ( "fmt" "net" )

func main() { fmt.Println(net.IPv4(8, 8, 8, 8)) } ```

輸出結果:

8.8.8.8

Brad Fitzpatrick 在實際編寫和使用時,發現 net 標準庫的型別有很多問題,很不好用。

現在有什麼問題

Brad Fitzpatrick 對於標準庫 net.IP 的問題,直接在文章中列舉了出來,論據十足。

共 7 個大問題:

  1. 它是可變的。 net.IP 的底層型別是 []byte,這意味著你傳遞給它的任何東西都可能改變它。
  2. 它不具有可比性。因為 Go 中的 slice 不具有可比性,這意味著 net.IP 不支援 Go 的 == 運算子的對比,不能作為 map 的 key 來使用。
  3. 它有兩種 IP 地址型別,要糾結用 net.IP,還是 net.IPAddr,要選擇就會很煩人。
  4. 它很大。Go 的 net.IP 包含 2 個部分,分別是 24 位元組的 slice header 和 4/6 位元組的 IP 地址。如果是 net.IPAddr 還會包含 Zone 欄位。
  5. 它會在堆上分配記憶體。Go 的 net 包到處都是分配,把更多的工作放在了 GC 上。
  6. 它不可解析。從字串形式解析 IP 時,Go 的 IP 型別無法區分 IPv4 對映的 IPv6 地址和 IPv4 地址。
  7. 它是透明型別(transparent type),net.IP 的定義是:type IP []byte,是其公共API的一部分,不可更改。

Brad 也有提到有些是當年早期的設計,當時經驗不足,或是沒有考慮好。

現在受限於 Go1 相容性承諾,已經無法改變了(相容性保障的雙刃劍?)。

這是個真實版 “Eating your own dog food”,所以在 Tailscale 他又重新造了一個輪子 inetaf/netaddr,想貢獻出來,塞進標準庫裡。

未來想要的樣子

對比表格如下:

| 特性 | 老方案 net.IP | 新方案 | | ---- | ---- | ---- | | 不變的 | ❌, slice | ✅ | | 可比的 | ❌, slice | ✅| | 佔用空間小 | ❌,28~56 位元組 | ✅,固定 24 位元組| | 不在堆上分配 | ❌ | ✅| | 支援 IPv4 和 IPv6 | ✅ | ✅ | | 區分 IPv4 和 IPv6 | ❌ | ✅ | | 支援 IPv6 區域 | ❌ | ✅ | | 不透明的型別 | ❌ | ✅ | | 與標準庫互通 | ✅ | 🤷,需適配方法 |

想要的樣子,其實是 Brad 業務實戰出來的訴求,就是要支援前面提到的 7 點。

解決方案

當前的進展

實現的結果,也就是新方案做出來了,他就是 inetaf/netaddr 這個庫(當然,也不排除是結果倒推理論)。並且在 Go issues 中發起 issues 和 proposal。

https://pkg.go.dev/inet.af/netaddr

Russ Cox 發起了新提案的討論《proposal: net/netaddr: add new IP address type, netaddr package (discussion)》,並被接納,進入了 Go1.18 的新特性當中。

重造過程

新的 net/netip 庫的每一個考量點,Brad 都在文章中有所詳細講解。

受限於篇幅,我們拿其中兩點來分享,有興趣的小夥伴可以閱讀原文的剖析部分。

介面型別組合

在可比較這事上,Go 的介面(interface)其實是支援比較的,也就是可以作為 map 的 key 進行 == 運算子的比較。

實現瞭如下的第一版方案,設計了新的 netaddr.IP 型別:

```go 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 } ```

上述程式碼,在 IP 結構體中增加了 ipImpl 介面,既能支援比較,還可以不對外暴露(不透明型別),且可以支援 IPv6。

新的問題在於,雖然比原生 net 小了,但還是沒達到目標,還是有在堆上分配的缺點。

免分配的 24 位元組

如果繼續使用介面,是無法解決根本目標(Brad 的目標是 24 位元組)的。

因為介面(interface)佔用 16 位元組,剩餘 8 個位元組可以用,要放如下東西: - 地址族(v4、v6,或兩者都不是,如:IP 的零值),至少需要 2 位。 - IPv6 的 zone 資訊。

還要能比較,顯然介面是無法實現的,因為地址+zone 資訊算一下位元組數,顯示是不夠用的。

正規顯式的沒辦法,Brad 想到了用打包的方式:

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

但這麼做,就意味著 zoneAndFamily 欄位中需要計算位數,再對應的推入相應的值,但也未必太折騰了。

最終 Brad 想到了,可以使用指標的方式:

go type IP struct { addr [16]byte zoneAndFamily *T }

再定義 3 個對應哨位值的來應用:

go var ( z0 *intern.Value // 表示零值。 z4 = new(intern.Value) // 表示 IPv4 的哨位值 z6noz = new(intern.Value) // 表示 IPv6 的哨位值(沒有 zone)。 )

這樣就可以把 IP 型別固定在 24 位元組。

總結

這個網路地址庫,一般都用的比較少。但是 Brad Fitzpatrick 在此投入了大量的精力和研究,達到了最終的目標。

除去庫的功能外,有許多技術優化點值得我們學習和參考,有興趣深入優化部分的,可以閱讀:https://tailscale.com/blog/netaddr-new-ip-type-for-go/

本文介紹的新 net/netip 庫將會在 Go1.18 中作為新特性出現,歡迎大家一起學習交流:)

若有任何疑問歡迎評論區反饋和交流,最好的關係是互相成就,各位的點贊就是煎魚創作的最大動力,感謝支援。

文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。