Go1.18 新特性:引入新的 netip 网络库

·  阅读 2959

大家好,我是煎鱼。

写这篇文章时是大年初一,原本想说这个月就要发布 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

示例代码:

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 类型:

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 想到了用打包的方式:

type IP struct {
   addr          [16]byte
   zoneAndFamily uint64
}
复制代码

但这么做,就意味着 zoneAndFamily 字段中需要计算位数,再对应的推入相应的值,但也未必太折腾了。

最终 Brad 想到了,可以使用指针的方式:

type IP struct {
    addr          [16]byte
    zoneAndFamily *T
}
复制代码

再定义 3 个对应哨位值的来应用:

var (
     z0    *intern.Value        // 表示零值。
     z4    = new(intern.Value)  // 表示 IPv4 的哨位值
     z6noz = new(intern.Value)  // 表示 IPv6 的哨位值(没有 zone)。
)
复制代码

这样就可以把 IP 类型固定在 24 字节。

总结

这个网络地址库,一般都用的比较少。但是 Brad Fitzpatrick 在此投入了大量的精力和研究,达到了最终的目标。

除去库的功能外,有许多技术优化点值得我们学习和参考,有兴趣深入优化部分的,可以阅读:tailscale.com/blog/netadd…

本文介绍的新 net/netip 库将会在 Go1.18 中作为新特性出现,欢迎大家一起学习交流:)

若有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改