投稿日:
更新日:

Go 1.18 で追加された net/netip パッケージ

Authors

目次

banner.gif

はじめに

Go 1.18"our biggest change ever to the language" と言われるほど、Go 言語誕生以来、最大の仕様拡張が行われました。

go1.18-release-twitter-card.png

特に注目を浴びたのは Generics でしょうか。 以前の Go は型パラメータを持たず、汎用的なデータ構造や関数を実装する際は、interface{} や型アサーションを使用するため、冗長な書き方が必要でした。

// 以前の書き方
func SumInts(m map[string]int) int {
    sum := 0
    for _, v := range m {
        sum += v
    }
    return sum
}

func SumFloats(m map[string]float64) float64 {
    sum := 0.0
    for _, v := range m {
        sum += v
    }
    return sum
}

これに対し、Go Generics では、型パラメータを活用して汎用化することができるため、型安全性や再利用性が大幅に向上し、コードを簡素化することができます。

// Generics を使用した書き方
func SumNumbers[T int | float64](m map[string]T) T {
    var sum T
    for _, v := range m {
        sum += v
    }
    return sum
}

ここで、T int | float64 という型セットを定義すると、型パラメータ T は、intfloat64 の両方の型を振る舞うことができます。

そして、もう一つ Go 1.18 では Go のパフォーマンスを向上させる重要なパッケージが追加されました。

それは netip パッケージ の登場です。 netip パッケージの追加は Go でネットワークプロトコルを書いている身として非常に画期的だと思いました。

これまで、Go を用いて IP アドレスやパケット処理を実装する際は、主に net パッケージを使用していましたが、言語的な特性により、処理やパフォーマンスに関していくつかの課題がありました。

netip パッケージは、net パッケージの課題を解決し、より使いやすく、より IP 操作のパフォーマンスを向上させるための機構が追加されています。

今回のブログでは、netip の導入背景を踏まえ、パッケージの特徴、互換性、メソッドの使い方等、従来の net パッケージと何が変わったのかを紹介したいと思います。

特徴

netip-package-ogp.png

Compared to the net.IP type, Addr type takes less memory, is immutable, and is comparable (supports == and being a map key).

netip パッケージに追加されている Addr 型には、従来の net パッケージの IP 型に対して次のような特徴があります。

  • 省メモリ
  • メモリアロケーションが発生しない
  • イミュータブル
  • == 演算子で比較可能
  • map の key に使用可能

netip パッケージの登場背景

netip パッケージの追加は、元 Go チーム、現在 Tailscale, Inc.bradfitz 氏 からの提案で始まりました。

netip-package-proposal.png

この Issue は実際に bradfitz 氏 が提案した、net パッケージにおける IP アドレスの表現方式を改善するために、inet.af/netaddr パッケージ を標準ライブラリに取り込み、netaddr.IP 型を導入するというものでした。

ここでは net.IP の課題と対応策について、次のように言及されています。

比較処理の非効率性

net.IP は可変長のバイトスライス([]byte)で表現されているため、Go の組み込みの比較演算子(==)を直接使用できません。 そのため、IP アドレスを比較するたびに手動でバイト列の要素を 1 つずつチェックする必要があります。

例えば、通常の整数や構造体であれば == を使って直接比較できるところを、net.IP では bytes.Equal を使って比較しなければなりません。

ip1 := net.ParseIP("192.168.1.1")
ip2 := net.ParseIP("192.168.1.1")

// 直接の比較はコンパイルエラーになる
// if ip1 == ip2 { ... }

// bytes.Equal で比較する
if bytes.Equal(ip1, ip2) {
    fmt.Println("IP addresses are equal")
}

bytes.Equal はスライスのすべてのバイトを走査するため、比較操作が遅くなります。

また、== を使えないため、可読性が悪くなりバグを引き起こしやすいという実装上の課題も生じます。

メモリアロケーションの発生

net.IP はスライスなので、値をコピーする際にメモリアロケーションが発生することで不要なメモリリソースを喰う可能性があります。

例えば、IP アドレスを map の key として使用する場合、スライスのポインタを格納するため、不要なメモリアロケーションが発生しやすくなります。

スライスは直接、map の key として扱うことができないため IP 型を string 型にキャストしてから格納します。

ipMap := make(map[string]string)
ip := net.ParseIP("192.168.1.1")

ipMap[ip] = "IPv4"          // NG: IP は map の key にできない
ipMap[ip.String()] = "IPv4" // OK: []byte を string に変換する

ここで ip.String() を呼び出すと、内部的には新たに string 型 16byte 分を確保するため、追加のメモリアロケーションが発生します。

UDP パフォーマンスの低下

比較処理の非効率性や不要なメモリアローケーションにより、特に ベストエフォート指向でコネクションレスに大量のパケットを処理する UDP ではパフォーマンス低下が顕著 になります。

TailscaleWireguard という OSS がベースになっています。 Wireguard は Go で実装された次世代の VPN ソリューションで、トンネリング の構築に UDP を使用します。

wireguard.png

また、HTTP/3 の普及が進む中、Google が開発した QUIC:Quick UDP Internet Connections も Go で実装されており、UDP ベースで動作します。

quic.png

今回、特に netip パッケージに惹かれたのはこの部分で、僕が研究開発に携わっている CYPHONIC:CYber PHysical Overlay Network over Internet Communication という Peer-to-Peer に基づくゼロトラストセキュリティネットワークを構築するためのオーバーレイネットワークプロトコルも Go で実装しており、UDP ベースとなっています。

cyphonic.png

netaddr.IP 型の提案

netaddr.IP 型は固定長の構造体(struct)を用いることで、上記の問題を解決するというものです。

inet.af/netaddr は、現 net/netip の前身にあたり、元々 Tailscale で導入されていた IP 操作に特化したパッケージです。

netaddr.IP 型は、構造体を用いることで、== 演算子による比較ができます。

type IP struct {
	addr uint128
	z *intern.Value
}

また、固定長な構造体のコピーはスタックを使用するため、ヒープ領域を使用するスライスと比較してメモリリソースの削減や Go コンパイラの最適化が可能です。

ipMap := make(map[netaddr.IP]string)
ip := netaddr.MustParseAddr("192.168.1.1")

ipMap[ip] = "IPv4" // メモリアロケーションが発生しない

命名規則の議論

当初の提案では、netaddr.IP という名称が検討されました。 これは、Go の net パッケージにある net.IP に対応する形で、新たな IP 型として導入しようという試みです。

しかし、netaddr というパッケージ名は net パッケージと独立しているものの、addr.IP という命名には違和感があるとの意見が出ました。

その後、シンプルな ip.Addr という名称が提案されましたが、今度は ip をパッケージ名にしてしまうと、変数名として一般的に使用される ip と衝突する可能性があるという問題が指摘されました。

その結果、最終的に netip.Addr という名称が採用されました。 この命名により、net パッケージとは明確に分離されつつも、IP アドレス型の表現として適切であると判断されました。

また、net パッケージ自体が既に大きく複雑になっているため、新たな IP 型を独立した netip パッケージとして切り出すことは合理的だとされました。

実際に利用する際は、net/netip をインポートします。

import "net/netip"

ちなみに前進の netaddr パッケージは非推奨となり、2023 年 5 月にはリポジトリもアーカイブされていました。

netaddr-package-archived.png

netip.Addr 型の構造

type Addr struct {
    addr uint128
    z *intern.Value
}

netip.Addr 型は構造体で、IPv4 アドレス、IPv6 アドレスとゾーンインデックス を表現します。 addr は IPv4 アドレスまたは IPv6 アドレスを管理し、z フィールドはゾーン(後述)を管理します。

net パッケージでは net.IP 型とゾーンを持つ net.IPAddr 型をそれぞれ定義しているのに対して、netip.Addr 型は構造体で両方を管理しています。

addr uint128

type Addr struct {
	// addr is the hi and lo bits of an IPv6 address. If z==z4,
	// hi and lo contain the IPv4-mapped IPv6 address.
	//
	// hi and lo are constructed by interpreting a 16-byte IPv6
	// address as a big-endian 128-bit number. The most significant
	// bits of that number go into hi, the rest into lo.
	//
	// For example, 0011:2233:4455:6677:8899:aabb:ccdd:eeff is stored as:
	// addr.hi = 0x0011223344556677
	// addr.lo = 0x8899aabbccddeeff
	//
	// We store IPs like this, rather than as [16]byte, because it
	// turns most operations on IPs into arithmetic and bit-twiddling
	// operations on 64-bit registers, which is much faster than
	// bytewise processing.
	addr uint128
	// 略
	z *intern.Value
}

// uint128 represents a uint128 using two uint64s.
//
// When the methods below mention a bit number, bit 0 is the most
// significant bit (in hi) and bit 127 is the lowest (lo&1).
type uint128 struct {
	hi uint64
	lo uint64
}

uint128 は、netip パッケージに定義されている 128bit(16byte)を表現するカスタム型です。

IPv6 アドレスは 128bit(16byte)で構成されており、z フィールドが v4(IPv4)の場合には ::ffff:192.168.1.1 のような IPv4-Mapped IPv6 Address の形式で保持されます。

  • IPv4-Mapped IPv6 Address
|                80 bits               | 16 |      32 bits        |
+--------------------------------------+--------------------------+
|0000..............................0000|FFFF|    IPv4 address     |
+--------------------------------------+----+---------------------+

ここで、IPv6 アドレスが 0011:2233:4455:6677:8899:aabb:ccdd:eeff の場合、addr.hi = 0x0011223344556677 / addr.lo = 0x8899aabbccddeeff とそれぞれ保存されます。

IPv6 は 128bit(16 オクテット)で表現されるため [16]byte のバイト配列を使用すれば良いのではと思うかもしれませんが、uint64 2 つで表現すると、64bit CPU では 2 つのレジスタで IPv6 を表現できます。 そのため、メモリアクセスやビット演算のオーバーヘッドが少なくなり、IP アドレスに対する算術演算を高速化できます。

64bit-register-operation.png
  • 比較が簡単
func (a IPv6Addr) Equals(b IPv6Addr) bool {
    return a.high == b.high && a.low == b.low
}
  • ビット演算(マスク処理)の簡略化
func (ip IPv6Addr) Mask(mask IPv6Addr) IPv6Addr {
    return IPv6Addr{
        high: ip.high & mask.high,
        low:  ip.low & mask.low,
    }
}

これをバイト配列で表現した場合、16 回の for ループで AND 演算を取る必要があります。

func Mask(a, b [16]byte) [16]byte {
    var result [16]byte
    for i := 0; i < 16; i++ {
        result[i] = a[i] & b[i]
    }
    return result
}

ベンチマーク結果:#63: Change the representation of IP to a pair of uint64s

z *intern.Value

type Addr struct {
	addr uint128
	// 略
	// z is a combination of the address family and the IPv6 zone.
	//
	// nil means invalid IP address (for a zero Addr).
	// z4 means an IPv4 address.
	// z6noz means an IPv6 address without a zone.
	//
	// Otherwise it's the interned zone name string.
	z *intern.Value
}

// z0, z4, and z6noz are sentinel IP.z values.
// See the IP type's field docs.
var (
	z0    = (*intern.Value)(nil)
	z4    = new(intern.Value)
	z6noz = new(intern.Value)
)

z フィールドは、z0z4z6noz 変数が定義されており、z0 は不正な IP アドレス、z4 は IPv4 アドレス、z6noz はゾーンインデックスを持たない IPv6 アドレスを示します。 さらに z フィールドには、任意のゾーンインデックスを表す文字列を保存することが可能で、その際はゾーンインデックス付きの IPv6 アドレスであることを示します。

ゾーンインデックス(Zone Index) とは、パケットを送信するネットワークインターフェースを指定するための識別子です。 パケットはルーティングテーブルの情報を参照して IP アドレスのゾーンに応じて、送信するネットワークインターフェースを決定します。

ゾーンインデックスは、特に IPv6 の LLA(Link Local Address) 等において重要になります。 IPv6 ネットワークでは、仕組み上、異なるネットワークに同一の LLA が存在する場合があります。

例:

  • eth0(ネットワーク 1)に接続された機器 A の IP:fe80::1ff:fe23:4567:890a
  • eth1(ネットワーク 2)に接続された機器 B の IP:fe80::1ff:fe23:4567:890a

この場合、送信先の IP アドレスが同じであるため、どのネットワークインターフェースを使用するべきか、アドレスのみでは判断できません。

そこで、fe80::1ff:fe23:4567:890a%eth0fe80::1ff:fe23:4567:890a%eth1 のように IPv6 アドレスの末尾に % ゾーンインデックスを指定することで、ネットワークインターフェースを指定します。 これにより、インターフェースが接続する先のネットワークを一意に特定することができます。

ここで、fe80::1ff:fe23:4567:890a%eth0 の場合、netip.Addr 型の z フィールドに eth0 という値が格納されます。

これは ping6 コマンドでも同様です。 ping6 は宛先 IP アドレスの末尾に、% で使用する送信元のインターフェースが指定されます。

$ ping6 -c 5 fe80::cd9:23bb:538b:5fa7
PING6(56=40+8+8 bytes) fe80::cd9:23bb:538b:5fa7%en0 --> fe80::cd9:23bb:538b:5fa7
16 bytes from fe80::cd9:23bb:538b:5fa7%en0, icmp_seq=0 hlim=64 time=0.255 ms
16 bytes from fe80::cd9:23bb:538b:5fa7%en0, icmp_seq=1 hlim=64 time=0.470 ms
16 bytes from fe80::cd9:23bb:538b:5fa7%en0, icmp_seq=2 hlim=64 time=0.385 ms
16 bytes from fe80::cd9:23bb:538b:5fa7%en0, icmp_seq=3 hlim=64 time=0.437 ms
16 bytes from fe80::cd9:23bb:538b:5fa7%en0, icmp_seq=4 hlim=64 time=0.390 ms

--- fe80::cd9:23bb:538b:5fa7 ping6 statistics ---
5 packets transmitted, 5 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 0.255/0.387/0.470/0.073 ms

intern.Value

z フィールドは intern.Value 型です。

intern.Value は以下のような型定義を取るカスタム型です。

// A Value pointer is the handle to an underlying comparable value.
// See func Get for how Value pointers may be used.
type Value struct {
	_      [0]func() // prevent people from accidentally using value type as comparable
	cmpVal any
	// resurrected is guarded by mu (for all instances of Value).
	// It is set true whenever v is synthesized from a uintptr.
	resurrected bool
}

ここで、[0]func() は長さゼロの関数型配列です。 Go では、ゼロ長の配列自体はメモリを消費せず、ただの型制約として機能します。 関数型 func() は比較不可能(non-comparable)な型であるため、これを言わばダミーフィールドとして Value 構造体内に含めることで Value 自体を Go のコンパイラが比較できないようにするというトリックを使っています。 つまり、Value は 常にポインタ *Value で管理されることになります。

また、Go 1.18 以降、any 型は interface{} のエイリアス型であり、どんな値でも格納することが可能です。 cmpVal には、比較対象となる値を格納することで間接的に比較を可能にします。

Addr 型が Value 構造体を採用する理由は、string 型よりも 8byte 分メモリフットプリントを小さくするためです。

Go の string 型は、ポインタ + 長さ で構成され、それぞれが 8byte となっています。

string の構造(16byte)
┌──────────────┬──────────────┐
│ pointer (8)  │ length (8)   │
└──────────────┴──────────────┘

つまり、string 型の比較には、ポインタと長さの両方を使用する必要があるわけです。

一方の、*intern.Value の場合、ポインタの参照先に実際の文字列が格納されることで、同じ文字列は同じポインタとなる ため、比較がポインタのアドレスのみで可能となります。 これにより、ポインタアドレスを比較することで同一文字列であるかを確認できます。

*intern.Value の構造(8byte)
┌──────────────┐
│ pointer (8)  │
└──────────────┘

なぜ構造体管理なのか

netip.Addr 型は構造体を採用することで次のようなメリットがあります。

省メモリ

従来の net パッケージの net.IP 型は合計 40byte、ゾーンを持つ net.IPAddr 型は 56byte でしたが、netip パッケージの netip.Addr 型は 24byte とメモリフットプリントが小さくなっています。

net.IP 型は、スライスであるため、長さや容量を管理する lencap の情報を持つ Slice Header の 24byte が必要となります。 これは IPv6 アドレスの 16byte よりも大きい値です。

slice-header-overhead.png

netip.Addr 型は構造体を採用することで、この問題を解決しています。

ip := make(net.IP, net.IPv6len)
sliceHeaderSize := int(unsafe.Sizeof(ip))
ipSize := len([net.IPv6len]net.IP{})
fmt.Printf("net.IP([%v]byte) Size = %vbyte\n", net.IPv6len, sliceHeaderSize+ipSize) // net.IP([16]byte) Size = 40byte

var addr netip.Addr
fmt.Printf("netip.Addr Size = %vbyte\n", unsafe.Sizeof(addr)) // netip.Addr Size = 24byte

メモリアロケーションが発生しない

netip.Addr 型は値レシーバ(func (a T) Method() {})を採用しています。

func (a netip.Addr) IsValid() bool {
    return a.z != 0
}

Go ランタイムの仕組み上、ポインタを持つ変数はヒープに割り当てられやすいですが、値渡し(Call by value)であればエスケープ解析によってスタック上で処理されます。

value-and-referece-call.png

これにより、ヒープアロケーションが発生しないためガベージコレクションの負担も抑えることができます。

また、他の関数に渡す場合も Call by value となります。

イミュータブル

イミュータブル(不変性)とは、関数または値の呼び出し先を書き換えても、呼び出し元の値が変わらない という性質です。

netip.Addr 型のメンバ変数はパッケージ外に非公開となっており、実体に対する値レシーバが用意されています。 値レシーバのコールはスタック上の操作となるため、呼び出し元の値は不変(Immutable)です。

// net.IP:アドレスは変わらないが値が変わっている
ip := net.ParseIP("192.168.1.1")
fmt.Printf("ip %v: %p\n", ip, ip) // ip 192.168.1.1: 0x1400000e170
ip[15] = 2
fmt.Printf("ip %v: %p\n", ip, ip) // ip 192.168.1.2: 0x1400000e170

// netip.Addr:アドレスと値のどちらも変わっている
addr := netip.MustParseAddr("192.168.1.1")
addrNext := addr.Next()
fmt.Printf("addr %v: %p\n", addr, &addr)             // addr 192.168.1.1: 0x1400000c198
fmt.Printf("addrNext %v: %p\n", addrNext, &addrNext) // addrNext 192.168.1.2: 0x1400000c1b0
immutability.png

== 演算子で比較可能

net.IP 型はスライスであるため直接的な比較ができませんが、netip.Addr 型は構造体であるため、== 演算子で比較ができます。

ip := net.ParseIP("192.168.1.1")
ip2 := net.ParseIP("192.168.1.2")
fmt.Printf("%v\n", ip == ip2) // NG: invalid operation: ip == ip2 (slice can only be compared to nil)
addr := netip.MustParseAddr("192.168.1.1")
addr2 := netip.MustParseAddr("192.168.1.1")
fmt.Printf("%v\n", addr == addr2) // true

addr3 := netip.MustParseAddr("192.168.1.2")
fmt.Printf("%v\n", addr == addr3) // false

map の key に使用可能

The comparison operators == and != must be fully defined for operands of the key type;

map の key は比較可能な型である必要があるため、netip.Addr 型では key として利用できます。

addr := netip.MustParseAddr("192.168.1.1")
addrKeyMap := map[netip.Addr]string{addr: "IPv4"}
fmt.Printf("%v\n", addrKeyMap) // map[192.168.1.1:IPv4]

net.IP 型との互換性

依存関係

Go の開発チームは将来的に、過去の API は内部で netip.Addr 型を使い、入出力だけを net.IP 型に変換して返したいと考えているようですが、Go 1.18 のリリース時点ではこの変更は入っていません。

また、net → netip への依存はありますが、逆はありません。

net.IP 型の IP アドレスに関する操作メソッドは、基本的に netip.Addr 型でも提供 されています。

以下は、与えられた IP アドレス(netip.Addr 型)がプライベート IP アドレスであるかを判定するメソッドの例です。

func (ip Addr) IsPrivate() bool {
	// Match the stdlib's IsPrivate logic.
	if ip.Is4() {
		// RFC 1918 allocates 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 as
		// private IPv4 address subnets.
		return ip.v4(0) == 10 ||
			(ip.v4(0) == 172 && ip.v4(1)&0xf0 == 16) ||
			(ip.v4(0) == 192 && ip.v4(1) == 168)
	}

	if ip.Is6() {
		// RFC 4193 allocates fc00::/7 as the unique local unicast IPv6 address
		// subnet.
		return ip.v6(0)&0xfe == 0xfc
	}

	return false // zero value
}

注意すべきは、net.IP 型を利用していた機能のすべてが netip.Addr 型で提供されているわけではないという点です。

例えば、LookupNetIP メソッドのように既存機能とほぼ同じ機能で、netip.Addr 型を返すようにしたものもありますが、すべてのメソッドに存在するわけではありません。

// LookupIP メソッドの netip.Addr バージョン
func (r *Resolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) {
	// TODO(bradfitz): make this efficient, making the internal net package
	// type throughout be netip.Addr and only converting to the net.IP slice
	// version at the edge. But for now (2021-10-20), this is a wrapper around
	// the old way.
	ips, err := r.LookupIP(ctx, network, host)
	if err != nil {
		return nil, err
	}
	ret := make([]netip.Addr, 0, len(ips))
	for _, ip := range ips {
		if a, ok := netip.AddrFromSlice(ip); ok {
			ret = append(ret, a)
		}
	}
	return ret, nil
}

UDP 関連のメソッド

本来の目的であった UDP 関連の処理を高速化するためにいくつかのメソッドが追加されました。

例えば、net パッケージの UDPConnをレシーバに取る 以下のメソッドは netip.Addr 型を使用して高速化しています。

  • ReadFromUDPAddrPort
func (c *UDPConn) ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error)
  • ReadMsgUDPAddrPort
func (c *UDPConn) ReadMsgUDPAddrPort(b, oob []byte) (n, oobn, flags int, addr netip.AddrPort, err error)
  • WriteToUDPAddrPort
func (c *UDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error)
  • WriteMsgUDPAddrPort
func (c *UDPConn) WriteMsgUDPAddrPort(b, oob []byte, addr netip.AddrPort) (n, oobn int, err error)

相互型変換

net.IP 型と netip.Addr 型は相互に変換が可能です。

  • net.IP → netip.Addr
ip := net.ParseIP("192.168.1.1")
if addr, ok := netip.AddrFromSlice(ip); ok {
	fmt.Printf("addr: %v\n", addr) // addr: ::ffff:192.168.1.1
}

ここで ::ffff:192.168.1.1 は IPv4-mapped IPv6 形式であるため、元の IPv4 アドレスを取り出すには Unmap を使用します。

fmt.Printf("addr: %v\n", addr.Unmap()) // addr: 192.168.1.1
  • netip.Addr → net.IP
addr := netip.MustParseAddr("192.168.1.1")
ip2 := net.IP(addr.AsSlice())
fmt.Printf("ip: %v\n", ip2)

netip.Addr 型の使い方

netip.Addr 型に関連する関数やメソッドを紹介します。

netip.Addr 型の表示

addr := netip.MustParseAddr("fe80::1%eth1")
fmt.Printf("addr: %v\n", addr)                                   // fe80::1%eth1
fmt.Printf("addr.Zone(): %v\n", addr.Zone())                     // eth1
fmt.Printf("addr.BitLen(): %v\n", addr.BitLen())                 // 128
fmt.Printf("addr.String(): %v\n", addr.String())                 // fe80::1%eth1
fmt.Printf("addr.StringExpanded(): %v\n", addr.StringExpanded()) // fe80:0000:0000:0000:0000:0000:0000:0001%eth1

netip.Addr 型の生成

parseAddr, err := netip.ParseAddr("192.168.1.1")
if err != nil {
	return err
}
fmt.Printf("parseAddr: %v\n", parseAddr)

mustParseAddr := netip.MustParseAddr("192.168.1.1")
fmt.Printf("mustParseAddr: %v\n", mustParseAddr)

ip := net.ParseIP("192.168.1.1")
fmt.Printf("ip: %v\n", ip)

var ipv4 [net.IPv4len]byte
copy(ipv4[:], ip[12:16])
addr4 := netip.AddrFrom4(ipv4)
fmt.Printf("addr4: %v\n", addr4)

var ipv6 [net.IPv6len]byte
copy(ipv6[:], ip[:])
addr16 := netip.AddrFrom16(ipv6)
fmt.Printf("addr16: %v\n", addr16)

if addr, ok := netip.AddrFromSlice(ip); ok {
	fmt.Printf("addr: %v\n", addr)
}

netip.Addr 型の変換

addr4 := netip.MustParseAddr("192.168.1.1")
fmt.Printf("addr4: %v\n", addr4)                     // 192.168.1.1
fmt.Printf("addr4.As4(): %v\n", addr4.As4())         // [192 168 1 1]
fmt.Printf("addr4.As16(): %v\n", addr4.As16())       // [0 0 0 0 0 0 0 0 0 0 255 255 192 168 1 1]
fmt.Printf("addr4.AsSlice(): %v\n", addr4.AsSlice()) // [192 168 1 1]

addr16 := netip.MustParseAddr("fe80::1%eth1")
fmt.Printf("addr16: %v\n", addr16)                     // fe80::1%eth1
// fmt.Printf("addr16.As4(): %v\n", addr16.As4())      // panic: As4 called on IPv6 address
fmt.Printf("addr16.As16(): %v\n", addr16.As16())       // [254 128 0 0 0 0 0 0 0 0 0 0 0 0 0 1]
fmt.Printf("addr16.AsSlice(): %v\n", addr16.AsSlice()) // [254 128 0 0 0 0 0 0 0 0 0 0 0 0 0 1]

アドレスの比較

addr1 := netip.MustParseAddr("192.168.1.1")
addr2 := netip.MustParseAddr("192.168.1.2")
fmt.Printf("192.168.1.1.Compare(192.168.1.2) = %v\n", addr1.Compare(addr2)) // -1
fmt.Printf("192.168.1.2.Compare(192.168.1.1) = %v\n", addr2.Compare(addr1)) // 1
fmt.Printf("192.168.1.1.Compare(192.168.1.1) = %v\n", addr1.Compare(addr1)) // 0
fmt.Printf("192.168.1.1.Less(192.168.1.2) = %v\n", addr1.Less(addr2))       // true
fmt.Printf("192.168.1.1.Is4() = %v\n", addr1.Is4())                         // true
fmt.Printf("192.168.1.1.Is6() = %v\n", addr1.Is6())                         // false
fmt.Printf("192.168.1.1.Is4In6() = %v\n", addr1.Is4In6())                   //false

// IPv4-Mapped IPv6 Address
addr3 := netip.MustParseAddr("::ffff:192.168.1.1")
fmt.Printf("::ffff:192.168.1.1.Is4() = %v\n", addr3.Is4())       // false
fmt.Printf("::ffff:192.168.1.1.Is6() = %v\n", addr3.Is6())       // true
fmt.Printf("::ffff:192.168.1.1.Is4In6() = %v\n", addr3.Is4In6()) // true
// Unmap することで IPv4-Mapped IPv6 Address が IPv4 アドレスになる
fmt.Printf("::ffff:192.168.1.1.Unmap().Is4() = %v\n", addr3.Unmap().Is4())       // true
fmt.Printf("::ffff:192.168.1.1.Unmap().Is6() = %v\n", addr3.Unmap().Is6())       // false
fmt.Printf("::ffff:192.168.1.1.Unmap().Is4In6() = %v\n", addr3.Unmap().Is4In6()) // false

アドレス種別の判別

// IPv6
fmt.Printf("2000::1.IsGlobalUnicast() = %v\n", netip.MustParseAddr("2000::1").IsGlobalUnicast())                     // true
fmt.Printf("ff01::1.IsInterfaceLocalMulticast() = %v\n", netip.MustParseAddr("ff01::1").IsInterfaceLocalMulticast()) // true
fmt.Printf("ff02::1.IsLinkLocalMulticast() = %v\n", netip.MustParseAddr("ff02::1").IsLinkLocalMulticast())           // true
fmt.Printf("fe80::1.IsLinkLocalUnicast() = %v\n", netip.MustParseAddr("fe80::1").IsLinkLocalUnicast())               // true
fmt.Printf("::1.IsLoopback() = %v\n", netip.MustParseAddr("::1").IsLoopback())                                       // true
fmt.Printf("ff02::1.IsMulticast() = %v\n", netip.MustParseAddr("ff02::1").IsMulticast())                             // true
fmt.Printf("fd00::1.IsPrivate() = %v\n", netip.MustParseAddr("fd00::1").IsPrivate())                                 // true
fmt.Printf("::.IsUnspecified() = %v\n", netip.MustParseAddr("::").IsUnspecified())                                   // true
fmt.Printf("fe80::1.IsValid() = %v\n", netip.MustParseAddr("fe80::1").IsValid())                                     // true
fmt.Printf("::.IsValid() = %v\n", netip.MustParseAddr("::").IsValid())                                               // true
// IPv4
fmt.Printf("192.0.2.1.IsGlobalUnicast() = %v\n", netip.MustParseAddr("192.0.2.1").IsGlobalUnicast())                     // true
fmt.Printf("224.0.0.1.IsInterfaceLocalMulticast() = %v\n", netip.MustParseAddr("224.0.0.1").IsInterfaceLocalMulticast()) // false(IPv4 に InterfaceLocalMulticast は存在しない)
fmt.Printf("224.0.0.1.IsLinkLocalMulticast() = %v\n", netip.MustParseAddr("224.0.0.1").IsLinkLocalMulticast())           // true
fmt.Printf("169.254.0.1.IsLinkLocalUnicast() = %v\n", netip.MustParseAddr("169.254.0.1").IsLinkLocalUnicast())           // true
fmt.Printf("127.0.0.1.IsLoopback() = %v\n", netip.MustParseAddr("127.0.0.1").IsLoopback())                               // true
fmt.Printf("224.0.0.1.IsMulticast() = %v\n", netip.MustParseAddr("224.0.0.1").IsMulticast())                             // true
fmt.Printf("192.168.1.1.IsPrivate() = %v\n", netip.MustParseAddr("192.168.1.1").IsPrivate())                             // true
fmt.Printf("0.0.0.0.IsUnspecified() = %v\n", netip.MustParseAddr("0.0.0.0").IsUnspecified())                             // true
fmt.Printf("192.168.1.1.IsValid() = %v\n", netip.MustParseAddr("192.168.1.1").IsValid())                                 // true
fmt.Printf("0.0.0.0.IsValid() = %v\n", netip.MustParseAddr("0.0.0.0").IsValid())                                         // true
// その他
fmt.Printf("netip.Addr{}.IsValid() = %v\n", netip.Addr{}.IsValid()) // false

IP アドレスの操作

addr4 := netip.MustParseAddr("192.168.1.2")
fmt.Printf("addr: %v\n", addr4)               // 192.168.1.2
fmt.Printf("addr.Prev(): %v\n", addr4.Prev()) // 192.168.1.1
fmt.Printf("addr.Next(): %v\n", addr4.Next()) // 192.168.1.3
addr4Prefix, err := addr4.Prefix(24)
if err != nil {
	return err
}
fmt.Printf("addr4.Prefix(): %v\n", addr4Prefix)                  // 192.168.1.0/24
fmt.Printf("addr4.WithZone(eth1): %v\n", addr4.WithZone("eth1")) // 192.168.1.2(IPv4 アドレスはゾーンに対応していない)

addr6 := netip.MustParseAddr("fe80::2")
fmt.Printf("addr6: %v\n", addr6)               // fe80::2
fmt.Printf("addr6.Prev(): %v\n", addr6.Prev()) // fe80::1
fmt.Printf("addr6.Next(): %v\n", addr6.Next()) // fe80::3
addr6Prefix, err := addr6.Prefix(24)
if err != nil {
	return err
}
fmt.Printf("addr6.Prefix(): %v\n", addr6Prefix)                  // fe80::/24
fmt.Printf("addr6.WithZone(eth1): %v\n", addr6.WithZone("eth1")) // fe80::2%eth1

Addr の Marshal と Unmarshal

addr4 := netip.MustParseAddr("192.168.1.1")
addr4b, err := addr4.MarshalBinary()
if err != nil {
	return err
}
fmt.Printf("addr4.MarshalBinary(): %v\n", addr4b) // [192 168 1 1]

var addr42 netip.Addr
if err := addr42.UnmarshalBinary(addr4b); err != nil {
	return err
}
fmt.Printf("addr42.UnmarshalBinary(addr4b): %v\n", addr42) // 192.168.1.1

addr4b2, err := addr4.MarshalText()
if err != nil {
	return err
}
fmt.Printf("addr4.MarshalText(): %v\n", addr4b2) // [49 57 50 46 49 54 56 46 49 46 49]

var addr43 netip.Addr
if err := addr43.UnmarshalText(addr4b2); err != nil {
	return err
}
fmt.Printf("addr43.UnmarshalText(addr4b2): %v\n", addr43) // 192.168.1.1

netip.AddrPort の構造

// AddrPort is an IP and a port number.
type AddrPort struct {
	ip   Addr
	port uint16
}

netip.AddrPort 型は、netip.Addr 型とともに port を持っています。

netip.AddrPort の使い方

AddrPort 型の生成

addr4 := netip.MustParseAddr("192.168.1.1")
addr4Port := netip.AddrPortFrom(addr4, 8080)
fmt.Printf("AddrPortFrom(): %v\n", addr4Port) // 192.168.1.1:8080

parseAddr4Port, err := netip.ParseAddrPort("192.168.1.1:8080")
if err != nil {
	return err
}
fmt.Printf("ParseAddrPort(192.168.1.1:8080): %v\n", parseAddr4Port) // 192.168.1.1:8080

mustParseAddr4Port := netip.MustParseAddrPort("192.168.1.1:8080")
fmt.Printf("MustParseAddrPort(192.168.1.1:8080): %v\n", mustParseAddr4Port) // 192.168.1.1:8080

アドレスポートの表示と判定

addr4Port := netip.MustParseAddrPort("192.168.1.1:8080")
fmt.Printf("addr4Port.String(): %v\n", addr4Port.String())   // 192.168.1.1:8080
fmt.Printf("addr4Port.Port(): %v\n", addr4Port.Port())       // 8080
fmt.Printf("addr4Port.Addr(): %v\n", addr4Port.Addr())       // 192.168.1.1
fmt.Printf("addr4Port.IsValid(): %v\n", addr4Port.IsValid()) // true
b := make([]byte, 0, len(addr4Port.String()))
fmt.Printf("addr4Port.AppendTo(b): %s\n", addr4Port.AppendTo(b)) // 192.168.1.1:8080

アドレスポートの比較

netip.Addr 型と同様に == 演算子で比較可能です。

addrPort := netip.MustParseAddrPort("192.168.1.1:8080")
addrPort2 := netip.MustParseAddrPort("192.168.1.1:8080")
addrPort3 := netip.MustParseAddrPort("192.168.1.2:8080")
addrPort4 := netip.MustParseAddrPort("192.168.1.1:443")
fmt.Printf("%v\n", addrPort == addrPort2) // true
fmt.Printf("%v\n", addrPort == addrPort3) // false(IP アドレスが異なる)
fmt.Printf("%v\n", addrPort == addrPort4) // false(ポートが異なる)

AddrPort の Marshal と Unmarshal

addr4Port := netip.MustParseAddrPort("192.168.1.1:8080")
addr4Portb, err := addr4Port.MarshalBinary()
if err != nil {
	return err
}
fmt.Printf("addr4Portb.MarshalBinary(): %v\n", addr4Portb) // [192 168 1 1 144 31]

var addr4Port2 netip.AddrPort
if err := addr4Port2.UnmarshalBinary(addr4Portb); err != nil {
	return err
}
fmt.Printf("addr4Port2.UnmarshalBinary(addr4Portb): %v\n", addr4Port2) // 192.168.1.1:8080

addr4Portb2, err := addr4Port.MarshalText()
if err != nil {
	return err
}
fmt.Printf("addr4Port.MarshalText(): %v\n", addr4Portb2) // [49 57 50 46 49 54 56 46 49 46 49 58 56 48 56 48]

var addr4Port3 netip.AddrPort
if err := addr4Port3.UnmarshalText(addr4Portb2); err != nil {
	return err
}
fmt.Printf("addr43.UnmarshalText(addr4Portb2): %v\n", addr4Port3) // 192.168.1.1:8080

netip.Prefix の構造

// Prefix is an IP address prefix (CIDR) representing an IP network.
//
// The first Bits() of Addr() are specified. The remaining bits match any address.
// The range of Bits() is [0,32] for IPv4 or [0,128] for IPv6.
type Prefix struct {
	ip Addr

	// bits is logically a uint8 (storing [0,128]) but also
	// encodes an "invalid" bit, currently represented by the
	// invalidPrefixBits sentinel value. It could be packed into
	// the uint8 more with more complicated expressions in the
	// accessors, but the extra byte (in padding anyway) doesn't
	// hurt and simplifies code below.
	bits int16
}

netip.Prefix は、CIDR(Classless Inter-domain Routing)を用いて IP アドレスをネットワークプレフィックスで管理する構造体です。

CIDR とは、クラスレスネットワークにおいて IP アドレスを 192.168.1.0/24 のように表現することです。

ここで、/24 は 192.168.1 までの 24bit がネットワークアドレスを表し、残りの 8bit がホストアドレスを表すことを意味します。

この時、192.168.1.0 が ip フィールドに保持され、24 が bits フィールドに保持されます。

netip.Prefix の使い方

Prefix 型の生成

parsePrefix, err := netip.ParsePrefix("192.168.1.0/24")
if err != nil {
	return err
}
fmt.Printf("ParsePrefix: %v\n", parsePrefix) // 192.168.1.0/24
mustParsePrefix := netip.MustParsePrefix("192.168.1.0/24")
fmt.Printf("MustParsePrefix: %v\n", mustParsePrefix) // 192.168.1.0/24
prefixFrom := netip.PrefixFrom(mustParsePrefix.Addr(), 24)
fmt.Printf("PrefixForm: %v\n", prefixFrom) // 192.168.1.0/24

操作

prefix := netip.MustParsePrefix("192.168.1.1/24")
fmt.Printf("prefix.Masked(): %v\n", prefix.Masked()) // 192.168.1.0/24
b := make([]byte, 0, len(prefix.String()))
fmt.Printf("prefix.AppendTo(b): %s\n", prefix.AppendTo(b)) // 192.168.1.1/24

プレフィックスの比較

netip.Addr 型 や netip.AddrPort 型と同様に == で比較可能です。

prefix := netip.MustParsePrefix("192.168.1.1/24")
prefix1 := netip.MustParsePrefix("192.168.1.1/24")
prefix2 := netip.MustParsePrefix("192.168.1.1/16")
fmt.Printf("192.168.1.1/24 == 192.168.1.1/24: %v\n", prefix == prefix1)               // true
fmt.Printf("192.168.1.1/24 == 192.168.1.1/16: %v\n", prefix == prefix2)               // false
fmt.Printf("192.168.1.1/24.Overlaps(192.168.1.1/16): %v\n", prefix.Overlaps(prefix2)) // true
prefix3 := netip.MustParsePrefix("172.16.0.0/12")
fmt.Printf("192.168.1.1/24.Overlaps(172.16.0.0/12): %v\n", prefix.Overlaps(prefix3)) // false
fmt.Printf("192.168.1.1/24.IsSingleIP(): %v\n", prefix.IsSingleIP())                 // false
prefix4 := netip.MustParsePrefix("192.168.1.1/32")
fmt.Printf("192.168.1.1/32.IsSingleIP(): %v\n", prefix4.IsSingleIP()) // true

プレフィックスベースの表示

prefix := netip.MustParsePrefix("192.168.1.1/24")
fmt.Printf("192.168.1.1/24.Addr(): %v\n", prefix.Addr())   // 192.168.1.1
fmt.Printf("192.168.1.1/24.Bits(): %v\n", prefix.Bits())   // 24
fmt.Printf("192.168.1.1/24.String(): %v\n", prefix.Bits()) // 192.168.1.1/24

Prefix の Marshal と Unmarshal

prefix := netip.MustParsePrefix("192.168.1.0/24")
prefixb, err := prefix.MarshalBinary()
if err != nil {
	return err
}
fmt.Printf("prefix.MarshalBinary(): %v\n", prefixb) // [192 168 1 0 24]

var prefix2 netip.Prefix
if err := prefix2.UnmarshalBinary(prefixb); err != nil {
	return err
}
fmt.Printf("prefix2.UnmarshalBinary(prefixb): %v\n", prefix2) // 192.168.1.0/24

prefixb2, err := prefix.MarshalText()
if err != nil {
	return err
}
fmt.Printf("prefix.MarshalText(): %v\n", prefixb2) // [49 57 50 46 49 54 56 46 49 46 48 47 50 52]

var prefix3 netip.Prefix
if err := prefix3.UnmarshalText(prefixb2); err != nil {
	return err
}
fmt.Printf("prefix3.UnmarshalText(prefix4b2): %v\n", prefix3) // 192.168.1.0/24

まとめ

今回のブログでは Go 1.18 で追加された netip パッケージについて紹介しました。

netip パッケージには、構造体で定義された Addr 型が追加されており、バイトスライスを使用する net パッケージ IP 型と比べて、メモリリソースやオーバーヘッドの削減、IP 操作に関する処理の簡素化が実現されています。 netip パッケージは UDP コネクションを使用する既存のメソッドへの導入が進められており、今後も net パッケージの内部処理は Addr 型への移行が進むと考えられます。

現在、研究開発している CYPHONIC も、これを気に net パッケージから netip パッケージに移行していきたいと思います。

参考・引用