フューチャー技術ブログ

Go1.18集中連載:新たに追加されたnet/netipとは

Image is generated By gopherize.me. Artwork by Ashley McNamara inspired by Renee French. Web app by Mat Ryer

この記事はGo1.18連載の4記事目です。

はじめに

こんにちは、TIG/DXユニット所属の宮永です。
本記事ではGo1.18で追加されたnet/netipの機能について解説します。

net/netipとは?

IPアドレスを対象とした基本的な操作(比較演算子による操作など)を提供するパッケージです。

netipパッケージに定義されている型は全部で3つ「Addr型(IPアドレスを定義)」「AddrPort型(IPアドレスとポートを定義)」「Prefix型(IPアドレスとビット長を定義)」です。

net/netipによって新たに導入された「Addr型」は「net.IP型」と比較してより少ないメモリでimmutableでかつ比較演算子を使って簡単に比較できるようになっていると公式のdocsには記載されています。

net/netipパッケージの導入提案はBradさんによってこちらのissueでされています。

私はnet.IPを使用したことはないのですが、従来のnetパッケージにはnet.IP型が実装されていました。net/netipの導入に伴ってnet.IP型の取り扱いやパッケージの命名方法などはかなり議論されていたようです。

proposal: net/netaddr: add new IP address type, netaddr package (discussion) · Discussion #47323 · golang/go

議論の争点は

  1. netパッケージに軽量なアドレスパッケージを追加するか?
  2. 汎用的なIPアドレス操作を担うパッケージを追加するか?

の2点でした(1)を採用する場合は今回追加されるパッケージ名は「net/netip」ではなく「net/netaddr」になっていたようです。

結論としてはnet/netipとして汎用的なIPアドレス操作を担うパッケージとしてgo1.18に取り入れることが決定したようです。

net/netipの利用用途は?

私自身、netパッケージはnet/http程度しか触ったことがないため従来のnet.IP型がどのように利用されていたのか、またどんなところに欠点があったのかを知りません。

net/netipの追加に大きな貢献をしたBradさんの技術ブログnetaddr.IP: a new IP address type for Go · Tailscaleにnet/netipの前身であるinetaf/netaddrを作成した経緯が記載されていましたのでこちらをベースにnet/netipの紹介をします。

Bradさんの記事では従来のnet.IP型の問題点はnet.IP型が単なるbyteのスライスでしか定義されていないことであると指摘されています。

この問題は、例えばIPアドレスを比較する際に==などの演算子は使用できないということを意味しています。

また、IPアドレスを表現するには不要な24バイトがnet.IPに割り当てられていることを指摘しています。
確かにgo1.17のnet.IP型を見てみると明確なサイズ制限はされていません。

// IP address lengths (bytes).
const (
IPv4len = 4
IPv6len = 16
)

// An IP is a single IP address, a slice of bytes.
// Functions in this package accept either 4-byte (IPv4)
// or 16-byte (IPv6) slices as input.
//
// Note that in this documentation, referring to an
// IP address as an IPv4 address or an IPv6 address
// is a semantic property of the address, not just the
// length of the byte slice: a 16-byte slice can still
// be an IPv4 address.
type IP []byte

一方でgo1.18beta2のnetip.Addrを確認するとaddruint128が明確に定義されています。

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

goにはuint128という型は存在しないため、uint64型2つを使用して定義しています。

// 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
}

uint64とは64ビット、つまりuint128で128ビット(=16バイト)を表現しています。

また、Addr型にはIPv6のゾーン識別子としてzというフィールドを用意しています。

// 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)
)

ゾーンを参照するZone()メソッドも用意されています。

// Zone returns ip's IPv6 scoped addressing zone, if any.
func (ip Addr) Zone() string {
if ip.z == nil {
return ""
}
zone, _ := ip.z.Get().(string)
return zone

ゾーンを定義する際にはWithZone()メソッドを使用します。

// WithZone returns an IP that's the same as ip but with the provided
// zone. If zone is empty, the zone is removed. If ip is an IPv4
// address, WithZone is a no-op and returns ip unchanged.
func (ip Addr) WithZone(zone string) Addr {
if !ip.Is6() {
return ip
}
if zone == "" {
ip.z = z6noz
return ip
}
ip.z = intern.GetByString(zone)
return ip
}

WithZone()メソッドは文字列からAddr型を定義するParseAddr()メソッドの内部でも利用されています。

// parseIPv6 parses s as an IPv6 address (in form "2001:db8::68").
func parseIPv6(in string) (Addr, error) {

...省略...
return AddrFrom16(ip).WithZone(zone), nil
}

net/netipを使う

それではnet/netipパッケージを実際に使ってみます。

まずは文字列からAddr型を生成します。ip0を空文字として不当なAddr型に、ip1をゾーン識別子(%eth0)付きのAddr型として設定します。

Addr型のメソッドであるIsValid()メソッドを使用していそれぞれの入力を評価します。

package main

import (
"fmt"
"net/netip"
)

func main() {
ip0, _ := netip.ParseAddr("")
ip1, _ := netip.ParseAddr("fe80::2%eth0")
fmt.Println(ip0.IsValid())
fmt.Println(ip1.IsValid())
}

以上のソースコードを実行すると

出力
false
true

が出力されます。

それでは次にIPv6アドレス(ゾーン識別子付き)を定義して比較演算子を使用してみます。

package main

import (
"fmt"
"net/netip"
)

func main() {
ip1, _ := netip.ParseAddr("fe80::2%eth0")
ip2, _ := netip.ParseAddr("fe80::2%eth0")
ip3, _ := netip.ParseAddr("192.0.2.1")
fmt.Println(ip1 == ip2)
fmt.Println(ip1 != ip2)
fmt.Println(ip1 == ip3)
}

以上のコードを実行すると

出力
true
false
false

と出力されます。net/netip導入の1つの目標である演算子による比較が可能になっています。

また、IPv6の表記ではゼロが2度以上続く場合「::」として省略できます。

省略せずに展開するメソッドとしてStringExpanded()などのメソッドも用意されています。StringExpanded()で返却される値は文字列です。

package main

import (
"fmt"
"net/netip"
)

func main() {
ip1, _ := netip.ParseAddr("fe80::2%eth0")
expIp1 := ip1.StringExpanded()
fmt.Println(expIp1)
fmt.Printf("%T\n", ip1)
fmt.Printf("%T\n", expIp1)
}

出力
fe80:0000:0000:0000:0000:0000:0000:0002%eth0
netip.Addr
string

ビット長もBitLen()メソッドを使えば簡単に調べることができます。

package main

import (
"fmt"
"net/netip"
)

func main() {
ip1, _ := netip.ParseAddr("fe80::2%eth0")
ip3, _ := netip.ParseAddr("192.0.2.1")
fmt.Println(ip1.BitLen())
fmt.Println(ip3.BitLen())
}
出力
128
32

簡単にIPアドレスの操作を行うことができますね。

まとめ

  • net/netipによって新たに導入された構造体は「Addr型(IPアドレスを定義)」「AddrPort型(IPアドレスとポートを定義)」「Prefix型(IPアドレスとビット長を定義)」の3つである。
  • net/netipではとnet.IPと比較してより少ないメモリでimmutableでかつ比較演算子を使って簡単に比較できるようになった。

今回Go1.18の集中連載記事を書くにあたってGoの公式リポジトリのissueやdocsなどを比較しながらまとめました。普段の実装では本家のソースコードをつぶさに確認することはなかったので良い体験ができたと思っています。「Goの実装をより良くするにはGo自体の実装を研究することだ」と誰かが言っていたのを思い出しました。

これを機に自分の普段の実装も見直してみたいと思います。

最後までお付き合いいただきありがとうございました。