[インデックス 17499] ファイルの概要
このコミットは、Go言語の標準ライブラリnetパッケージにおけるIPアドレスのマーシャリング(バイト列への変換)とアンマーシャリング(バイト列からの復元)に関する挙動の修正です。具体的には、nilのIPアドレスをencoding.TextMarshalerインターフェースを通じてマーシャリングする際にエラーを返さないように変更し、また空のバイト列をアンマーシャリングする際にnilのIPアドレスとして正しく処理するように修正しています。これにより、net.IP型のnil値の扱いが一貫性を持つようになり、特にJSONなどのテキストベースのフォーマットでIPアドレスを扱う際の利便性と堅牢性が向上しました。
コミット
commit da7a51d16be93ad91cfbc2d90c06d55bd847aa98
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Sep 6 15:29:09 2013 -0700
net: don't error when marshalling nil IP addresses
See https://code.google.com/p/go/issues/detail?id=6339#c3
Fixes #6339
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/13553044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/da7a51d16be93ad91cfbc2d90c06d55bd847aa98
元コミット内容
net: don't error when marshalling nil IP addresses
このコミットは、nilのIPアドレスをマーシャリングする際にエラーを発生させないようにするものです。これは、GoのIssue 6339に関連する修正であり、https://code.google.com/p/go/issues/detail?id=6339#c3で議論された内容に基づいています。
変更の背景
Go言語のnetパッケージにおけるIP型は、IPアドレスを表すバイトスライスです。この型は、encoding.TextMarshalerおよびencoding.TextUnmarshalerインターフェースを実装しており、これによりJSONなどのテキストベースのデータフォーマットとの間でIPアドレスを簡単に変換できます。
このコミット以前は、net.IP型の値がnil(つまり、IPアドレスが設定されていない状態)である場合にMarshalTextメソッドを呼び出すと、「invalid IP address」というエラーが返されていました。これは、IPアドレスが存在しない状態を表現したい場合に不便であり、特にデータベースのNULL値や、オプションのIPアドレスフィールドを扱う際に問題となりました。
GoのIssue 6339では、この挙動が議論され、nilのIPアドレスは空文字列としてマーシャリングされるべきであり、空文字列はnilのIPアドレスとしてアンマーシャリングされるべきであるという合意が形成されました。この変更は、nilのIPアドレスをエラーとして扱わず、空の表現として扱うことで、より柔軟で直感的なAPIを提供することを目的としています。
前提知識の解説
net.IP型
net.IPはGo言語のnetパッケージで定義されている型で、IPアドレス(IPv4またはIPv6)を表すバイトスライスです。例えば、IPv4アドレスは4バイト、IPv6アドレスは16バイトで表現されます。net.IP型の変数は、IPアドレスが設定されていない状態をnilで表現できます。
encoding.TextMarshalerインターフェース
encoding.TextMarshalerインターフェースは、Goの標準ライブラリencodingパッケージで定義されています。このインターフェースは、型が自身をテキスト形式にマーシャリングする方法を定義するために使用されます。
type TextMarshaler interface {
MarshalText() (text []byte, err error)
}
このインターフェースを実装する型は、json.Marshalなどの関数によってJSONにエンコードされる際に、MarshalTextメソッドが呼び出され、その結果がJSON文字列として出力されます。
encoding.TextUnmarshalerインターフェース
encoding.TextUnmarshalerインターフェースもencodingパッケージで定義されており、型が自身をテキスト形式からアンマーシャリングする方法を定義します。
type TextUnmarshaler interface {
UnmarshalText(text []byte) error
}
このインターフェースを実装する型は、json.Unmarshalなどの関数によってJSONからデコードされる際に、UnmarshalTextメソッドが呼び出され、入力されたテキストデータがその型の値に変換されます。
nilと空スライス
Goにおいて、nilスライスと空スライス([]byte{}やmake([]byte, 0))は異なる概念です。
nilスライスは、基底配列へのポインタがnilであり、長さと容量が0です。- 空スライスは、基底配列が存在し、長さと容量が0です。
このコミットでは、net.IPがnilの場合と、空のバイト列("")として表現される場合の両方を適切に扱うように変更されています。
技術的詳細
このコミットの主要な変更点は、src/pkg/net/ip.go内のIP.MarshalTextメソッドとIP.UnmarshalTextメソッドの挙動修正です。
IP.MarshalTextの変更
変更前は、IP.MarshalTextメソッドはlen(ip) != IPv4len && len(ip) != IPv6lenという条件でIPアドレスの長さが不正な場合にエラーを返していました。この条件はlen(ip) == 0(つまりnilのIPアドレス)の場合も含まれていました。
変更後は、if len(ip) == 0 { return []byte(""), nil }という行が追加されました。これにより、nilのIPアドレスが渡された場合、エラーを返さずに空のバイトスライス("")とnilエラーを返すようになりました。これは、nilのIPアドレスを空文字列として表現するという意図を反映しています。
IP.UnmarshalTextの変更
変更前は、IP.UnmarshalTextメソッドは空のテキストが渡された場合にParseIPがnilを返し、その結果x == nilの条件に合致して「invalid IP address」エラーを返していました。
変更後は、if len(text) == 0 { *ip = nil; return nil }という行が追加されました。これにより、空のバイトスライス("")が渡された場合、エラーを返さずに*ip = nilとしてIPアドレスをnilに設定し、nilエラーを返すようになりました。これは、空文字列をnilのIPアドレスとして解釈するという意図を反映しています。
テストケースの追加と修正
src/pkg/net/ip_test.goには、これらの変更を検証するための新しいテストケースTestMarshalEmptyIPが追加されました。このテストは、nilまたは空のバイトスライスをUnmarshalTextに渡した場合にnilのIPアドレスが正しく設定されること、およびnilのIPアドレスをMarshalTextでマーシャリングした場合に空のバイトスライスが返されることを確認します。
また、既存のTestIPStringテストでは、nilのIPアドレスのString()メソッドの期待値が"<nil>"から""に変更され、MarshalTextのテストロジックもnilの場合にエラーを期待するのではなく、空文字列を期待するように修正されました。
コアとなるコードの変更箇所
src/pkg/net/ip.go
--- a/src/pkg/net/ip.go
+++ b/src/pkg/net/ip.go
@@ -315,6 +315,9 @@ func (ip IP) String() string {
// MarshalText implements the encoding.TextMarshaler interface.
// The encoding is the same as returned by String.
func (ip IP) MarshalText() ([]byte, error) {
+ if len(ip) == 0 {
+ return []byte(""), nil
+ }
if len(ip) != IPv4len && len(ip) != IPv6len {
return nil, errors.New("invalid IP address")
}
@@ -324,6 +327,10 @@ func (ip IP) MarshalText() ([]byte, error) {
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The IP address is expected in a form accepted by ParseIP.
func (ip *IP) UnmarshalText(text []byte) error {
+ if len(text) == 0 {
+ *ip = nil
+ return nil
+ }
s := string(text)
x := ParseIP(s)
if x == nil {
src/pkg/net/ip_test.go
--- a/src/pkg/net/ip_test.go
+++ b/src/pkg/net/ip_test.go
@@ -32,14 +32,35 @@ func TestParseIP(t *testing.T) {
if out := ParseIP(tt.in); !reflect.DeepEqual(out, tt.out) {
t.Errorf("ParseIP(%q) = %v, want %v", tt.in, out, tt.out)
}
+ if tt.in == "" {
+ // Tested in TestMarshalEmptyIP below.
+ continue
+ }
var out IP
-
if err := out.UnmarshalText([]byte(tt.in)); !reflect.DeepEqual(out, tt.out) || (tt.out == nil) != (err != nil) {
t.Errorf("IP.UnmarshalText(%q) = %v, %v, want %v", tt.in, out, err, tt.out)
}
}
}
+// Issue 6339
+func TestMarshalEmptyIP(t *testing.T) {
+ for _, in := range [][]byte{nil, []byte("")} {
+ var out = IP{1, 2, 3, 4}
+ if err := out.UnmarshalText(in); err != nil || out != nil {
+ t.Errorf("UnmarshalText(%v) = %v, %v; want nil, nil", in, out, err)
+ }
+ }
+ var ip IP
+ got, err := ip.MarshalText()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(got, []byte("")) {
+ t.Errorf(`got %#v, want []byte("")`, got)
+ }
+}
+
var ipStringTests = []struct {
in IP
out string // see RFC 5952
@@ -53,23 +74,19 @@ var ipStringTests = []struct {
{IP{0x20, 0x1, 0xd, 0xb8, 0, 0, 0, 0, 0, 0x1, 0, 0, 0, 0, 0, 0x1}, "2001:db8::1:0:0:1"},
{IP{0x20, 0x1, 0xD, 0xB8, 0, 0, 0, 0, 0, 0xA, 0, 0xB, 0, 0xC, 0, 0xD}, "2001:db8::a:b:c:d"},
{IPv4(192, 168, 0, 1), "192.168.0.1"},
- {nil, "<nil>"},
+ {nil, ""},
}
func TestIPString(t *testing.T) {
for _, tt := range ipStringTests {
- if out := tt.in.String(); out != tt.out {
- t.Errorf("IP.String(%v) = %q, want %q", tt.in, out, tt.out)
- }
if tt.in != nil {
- if out, err := tt.in.MarshalText(); string(out) != tt.out || err != nil {
- t.Errorf("IP.MarshalText(%v) = %q, %v, want %q, nil", out, err, tt.out)
- }
- } else {
- if _, err := tt.in.MarshalText(); err == nil {
- t.Errorf("IP.MarshalText(nil) succeeded, want failure")
+ if out := tt.in.String(); out != tt.out {
+ t.Errorf("IP.String(%v) = %q, want %q", tt.in, out, tt.out)
}
}
+ if out, err := tt.in.MarshalText(); string(out) != tt.out || err != nil {
+ t.Errorf("IP.MarshalText(%v) = %q, %v, want %q, nil", out, err, tt.out)
+ }
}
}
コアとなるコードの解説
src/pkg/net/ip.goの変更
-
func (ip IP) MarshalText() ([]byte, error):- 追加された
if len(ip) == 0 { return []byte(""), nil }は、ipスライスの長さが0の場合(つまり、nilまたは空スライスの場合)に、空のバイトスライスとnilエラーを返します。これにより、nilのIPアドレスがJSONなどでは空文字列として表現されるようになります。 - この変更により、以前は
len(ip) != IPv4len && len(ip) != IPv6lenの条件でnilのIPアドレスが「invalid IP address」エラーを返していた挙動が修正されました。
- 追加された
-
func (ip *IP) UnmarshalText(text []byte) error:- 追加された
if len(text) == 0 { *ip = nil; return nil }は、入力textが空のバイトスライスの場合に、*ipをnilに設定し、nilエラーを返します。これにより、空文字列がnilのIPアドレスとして正しくアンマーシャリングされるようになります。 - 以前は、空文字列が
ParseIPに渡されるとnilが返され、その結果「invalid IP address」エラーが発生していました。この変更により、その問題が解決されました。
- 追加された
src/pkg/net/ip_test.goの変更
-
func TestMarshalEmptyIP(t *testing.T):- この新しいテスト関数は、
nilまたは空のバイトスライスをUnmarshalTextに渡したときに、IP型の値が正しくnilになることを検証します。 - また、
nilのIP型の値をMarshalTextでマーシャリングしたときに、空のバイトスライスが返されることを検証します。これは、新しい挙動が期待通りであることを確認するための重要なテストです。
- この新しいテスト関数は、
-
var ipStringTestsの修正:{nil, "<nil>"}が{nil, ""}に変更されました。これは、nilのIPアドレスのString()メソッドが空文字列を返すように変更されたためです。
-
func TestIPString(t *testing.T)の修正:nilのIPアドレスに対するMarshalTextのテストロジックが変更されました。以前はnilの場合にエラーを期待していましたが、変更後は空文字列とnilエラーを期待するように修正されました。IP.String()のテストロジックも、nilの場合の期待値が""になるように調整されました。
これらの変更により、net.IP型がnilである場合のマーシャリングとアンマーシャリングの挙動が明確になり、encoding.TextMarshalerおよびencoding.TextUnmarshalerインターフェースの実装がより堅牢で使いやすくなりました。
関連リンク
- Go Code Review: https://golang.org/cl/13553044
- Go Issue 6339 (元のリンクは現在アクセスできませんが、コミットメッセージに記載されています):
https://code.google.com/p/go/issues/detail?id=6339#c3
参考にした情報源リンク
- Go言語の公式ドキュメント(
netパッケージ、encodingパッケージ) - Go言語のソースコード(
src/pkg/net/ip.go,src/pkg/net/ip_test.go) - Go言語のIssueトラッカー(Issue 6339に関する議論)
encoding.TextMarshalerおよびencoding.TextUnmarshalerインターフェースに関する一般的なGo言語のドキュメントや解説記事。