Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 17222] ファイルの概要

このコミットは、Go言語の標準ライブラリnetパッケージ内のIP型に、encoding.TextMarshalerおよびencoding.TextUnmarshalerインターフェースの実装を追加するものです。これにより、IPアドレスをテキスト形式でエンコード(文字列化)およびデコード(パース)する際の標準的な方法が提供されます。

コミット

commit 413d4c6c11e23b79a26465a840bce7b0f1392425
Author: Russ Cox <rsc@golang.org>
Date:   Wed Aug 14 00:33:20 2013 -0400

    net: make IP implement encoding.MarshalerText, encoding.UnmarshalerText
    
    See golang.org/s/go12encoding for design.
    
    R=golang-dev, bradfitz, mikioh.mikioh
    CC=golang-dev
    https://golang.org/cl/12705043
---
 src/pkg/net/ip.go      | 23 +++++++++++++++++++++++
 src/pkg/net/ip_test.go | 15 +++++++++++++++
 2 files changed, 38 insertions(+)

diff --git a/src/pkg/net/ip.go b/src/pkg/net/ip.go
index 0e42da2168..b73804d528 100644
--- a/src/pkg/net/ip.go
+++ b/src/pkg/net/ip.go
@@ -12,6 +12,8 @@
 
 package net
 
+import "errors"
+
 // IP address lengths (bytes).
 const (
 	IPv4len = 4
@@ -310,6 +312,27 @@ func (ip IP) String() string {
 	return s
 }
 
+// MarshalText implements the encoding.TextMarshaler interface.
+// The encoding is the same as returned by String.\n+func (ip IP) MarshalText() ([]byte, error) {\n+\tif len(ip) != IPv4len && len(ip) != IPv6len {\n+\t\treturn nil, errors.New(\"invalid IP address\")\n+\t}\n+\treturn []byte(ip.String()), nil\n+}\n+\n+// UnmarshalText implements the encoding.TextUnmarshaler interface.\n+// The IP address is expected in a form accepted by ParseIP.\n+func (ip *IP) UnmarshalText(text []byte) error {\n+\ts := string(text)\n+\tx := ParseIP(s)\n+\tif x == nil {\n+\t\treturn &ParseError{\"IP address\", s}\n+\t}\n+\t*ip = x\n+\treturn nil\n+}\n+\n // Equal returns true if ip and x are the same IP address.\n // An IPv4 address and that same address in IPv6 form are\n // considered to be equal.\ndiff --git a/src/pkg/net/ip_test.go b/src/pkg/net/ip_test.go
index 16f30d446b..606344d265 100644
--- a/src/pkg/net/ip_test.go
+++ b/src/pkg/net/ip_test.go
@@ -32,6 +32,11 @@ func TestParseIP(t *testing.T) {\n \t\tif out := ParseIP(tt.in); !reflect.DeepEqual(out, tt.out) {\n \t\t\tt.Errorf(\"ParseIP(%q) = %v, want %v\", tt.in, out, tt.out)\n \t\t}\n+\t\tvar out IP\n+\n+\t\tif err := out.UnmarshalText([]byte(tt.in)); !reflect.DeepEqual(out, tt.out) || (tt.out == nil) != (err != nil) {\n+\t\t\tt.Errorf(\"IP.UnmarshalText(%q) = %v, %v, want %v\", tt.in, out, err, tt.out)\n+\t\t}\n \t}\n }\n \n@@ -47,6 +52,7 @@ var ipStringTests = []struct {\n \t{IP{0x20, 0x1, 0xd, 0xb8, 0, 0, 0, 0, 0, 0x1, 0, 0, 0, 0, 0, 0}, \"2001:db8:0:0:1::\"},\n \t{IP{0x20, 0x1, 0xd, 0xb8, 0, 0, 0, 0, 0, 0x1, 0, 0, 0, 0, 0, 0x1}, \"2001:db8::1:0:0:1\"},\n \t{IP{0x20, 0x1, 0xD, 0xB8, 0, 0, 0, 0, 0, 0xA, 0, 0xB, 0, 0xC, 0, 0xD}, \"2001:db8::a:b:c:d\"},\n+\t{IPv4(192, 168, 0, 1), \"192.168.0.1\"},\n \t{nil, \"<nil>\"},\n }\n \n@@ -55,6 +61,15 @@ func TestIPString(t *testing.T) {\n \t\tif out := tt.in.String(); out != tt.out {\n \t\t\tt.Errorf(\"IP.String(%v) = %q, want %q\", tt.in, out, tt.out)\n \t\t}\n+\t\tif tt.in != nil {\n+\t\t\tif out, err := tt.in.MarshalText(); string(out) != tt.out || err != nil {\n+\t\t\t\tt.Errorf(\"IP.MarshalText(%v) = %q, %v, want %q, nil\", out, err, tt.out)\n+\t\t\t}\n+\t\t} else {\n+\t\t\tif _, err := tt.in.MarshalText(); err == nil {\n+\t\t\t\tt.Errorf(\"IP.MarshalText(nil) succeeded, want failure\")\n+\t\t\t}\n+\t\t}\n \t}\n }\n \n```

## GitHub上でのコミットページへのリンク

[https://github.com/golang/go/commit/413d4c6c11e23b79a26465a840bce7b0f1392425](https://github.com/golang/go/commit/413d4c6c11e23b79a26465a840bce7b0f1392425)

## 元コミット内容

net: make IP implement encoding.MarshalerText, encoding.UnmarshalerText

See golang.org/s/go12encoding for design.

R=golang-dev, bradfitz, mikioh.mikioh CC=golang-dev https://golang.org/cl/12705043


## 変更の背景

このコミットの背景には、Go 1.2リリースに向けたエンコーディング関連のインターフェース設計の標準化があります。特に、構造体やカスタム型をJSON、XML、YAMLなどの様々なデータ形式にエンコード・デコードする際に、その型がどのようにテキスト表現されるべきかを定義するための統一されたメカニズムが必要とされていました。

`net.IP`型は、IPアドレスを表現するためのバイトスライス(`[]byte`)のエイリアスですが、そのテキスト表現(例: "192.168.1.1" や "2001:db8::1")は`String()`メソッドによって提供されていました。しかし、この`String()`メソッドは単なる文字列変換であり、`encoding`パッケージが提供する標準的なエンコーディング/デコーディングの枠組みには適合していませんでした。

`encoding.TextMarshaler`と`encoding.TextUnmarshaler`インターフェースは、Go 1.2で導入された新しいインターフェースであり、任意の型が自身をテキスト形式に変換する方法(マーシャリング)と、テキスト形式から自身を再構築する方法(アンマーシャリング)を定義することを可能にします。これにより、`json.Marshal`や`yaml.Marshal`などの汎用的なエンコーダが、カスタム型を適切に処理できるようになります。

`net.IP`型がこれらのインターフェースを実装することで、`IP`アドレスをフィールドに持つ構造体をJSONや他のテキストベースの形式にエンコードする際に、自動的に人間が読めるIPアドレス文字列として扱われるようになります。同様に、テキスト形式のIPアドレス文字列をデコードする際にも、`net.IP`型として正しくパースされるようになります。これは、APIの設計やデータ永続化において、`IP`アドレスをより自然に扱うための重要な改善でした。

コミットメッセージにある `golang.org/s/go12encoding` は、Go 1.2におけるエンコーディングインターフェースの設計に関する提案文書へのリンクであり、この変更がより大きな設計思想の一部であることを示しています。

## 前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とネットワークに関する基礎知識が必要です。

### 1. Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たすと見なされます。Goのインターフェースは「暗黙的」であり、`implements`キーワードのような明示的な宣言は不要です。

このコミットでは、`encoding.TextMarshaler`と`encoding.TextUnmarshaler`という2つのインターフェースが重要です。

*   **`encoding.TextMarshaler`インターフェース**:
    ```go
    type TextMarshaler interface {
        MarshalText() (text []byte, err error)
    }
    ```
    このインターフェースを実装する型は、自身をテキスト形式のバイトスライスに変換する方法を提供します。`json.Marshal`などのエンコーダは、このインターフェースを実装している型を見つけると、`MarshalText`メソッドを呼び出してテキスト表現を取得します。

*   **`encoding.TextUnmarshaler`インターフェース**:
    ```go
    type TextUnmarshaler interface {
        UnmarshalText(text []byte) error
    }
    ```
    このインターフェースを実装する型は、テキスト形式のバイトスライスから自身を再構築する方法を提供します。`json.Unmarshal`などのデコーダは、このインターフェースを実装している型を見つけると、`UnmarshalText`メソッドを呼び出してテキストをパースし、型を初期化します。

### 2. `net.IP`型

Go言語の`net`パッケージは、ネットワーク関連のプリミティブを提供します。`net.IP`型は、IPアドレス(IPv4またはIPv6)を表すために使用されるバイトスライスです。

```go
type IP []byte
  • IP.String()メソッド: net.IP型には、IPアドレスを人間が読める文字列形式(例: "192.168.1.1" や "2001:db8::1")に変換するString()メソッドが既に存在します。これはfmt.Stringerインターフェースを暗黙的に満たします。

  • net.ParseIP()関数: 文字列形式のIPアドレスをnet.IP型にパースする関数です。無効なIPアドレス文字列が与えられた場合はnilを返します。

3. IPアドレスの基礎

  • IPv4: 32ビットのアドレス空間を持ち、通常はドット区切りの10進数(例: 192.168.1.1)で表現されます。net.IPv4lenは4バイト(32ビット)です。
  • IPv6: 128ビットのアドレス空間を持ち、通常はコロン区切りの16進数(例: 2001:db8::1)で表現されます。net.IPv6lenは16バイト(128ビット)です。

4. エラーハンドリング

Go言語では、エラーは関数の最後の戻り値としてerrorインターフェースを実装する型で返されます。このコミットでは、errors.Newを使用して新しいエラーを作成したり、ParseErrorというカスタムエラー型を使用したりしています。

技術的詳細

このコミットの技術的詳細は、net.IP型がencoding.TextMarshalerencoding.TextUnmarshalerインターフェースをどのように実装しているかに集約されます。

MarshalText()の実装

MarshalText()メソッドは、net.IP型の値をテキスト形式のバイトスライスに変換します。

  1. シグネチャ: func (ip IP) MarshalText() ([]byte, error)

    • レシーバは値レシーバ(ip IP)です。これは、MarshalTextIPの値を変更しないためです。
    • 戻り値は[]byte(テキスト表現)とerrorです。
  2. IPアドレスの長さチェック:

    if len(ip) != IPv4len && len(ip) != IPv6len {
        return nil, errors.New("invalid IP address")
    }
    

    net.IPはバイトスライスであるため、その長さがIPv4(4バイト)またはIPv6(16バイト)のいずれでもない場合、それは無効なIPアドレスと見なされ、エラーを返します。これは、不正なIP値がマーシャリングされるのを防ぐための堅牢性チェックです。

  3. String()メソッドの利用:

    return []byte(ip.String()), nil
    

    有効なIPアドレスの場合、既存のip.String()メソッドを呼び出してIPアドレスの標準的なテキスト表現を取得し、それをバイトスライスに変換して返します。これにより、MarshalTextの出力形式がString()と同じであることが保証されます。

UnmarshalText()の実装

UnmarshalText()メソッドは、テキスト形式のバイトスライスからnet.IP型の値を再構築します。

  1. シグネチャ: func (ip *IP) UnmarshalText(text []byte) error

    • レシーバはポインタレシーバ(ip *IP)です。これは、UnmarshalTextがレシーバの値を変更する必要があるためです。インターフェースの規約により、アンマーシャリングメソッドはポインタレシーバを持つ必要があります。
    • 引数はtext []byte(入力テキスト)です。
    • 戻り値はerrorです。
  2. バイトスライスから文字列への変換:

    s := string(text)
    

    入力されたバイトスライスtextをGoの文字列sに変換します。これは、既存のParseIP関数が文字列を引数として取るためです。

  3. ParseIP()関数の利用:

    x := ParseIP(s)
    

    変換された文字列snet.ParseIP()関数に渡して、IPアドレスをパースします。ParseIPは有効なIPアドレスであればnet.IP型の値を返し、無効であればnilを返します。

  4. パース結果のチェックとエラーハンドリング:

    if x == nil {
        return &ParseError{"IP address", s}
    }
    

    ParseIPnilを返した場合(つまり、入力テキストが無効なIPアドレス形式であった場合)、ParseErrorというカスタムエラー型を返します。これは、パースに失敗したことを明確に示します。

  5. レシーバへの値の代入:

    *ip = x
    

    パースが成功した場合、ParseIPが返したnet.IP型の値xを、ポインタレシーバipが指す元のIP変数に代入します。これにより、呼び出し元のIP変数が更新されます。

テストの追加

src/pkg/net/ip_test.goには、これらの新しいメソッドの動作を検証するためのテストケースが追加されています。

  • TestParseIP関数内では、既存のParseIPのテストループにUnmarshalTextのテストが追加されています。これにより、UnmarshalTextParseIPと同じ結果を生成し、エラーハンドリングも適切に行われることが確認されます。
  • TestIPString関数内では、既存のIP.String()のテストループにMarshalTextのテストが追加されています。これにより、MarshalTextIP.String()と同じ文字列を生成し、無効なIP値(nil)に対してはエラーを返すことが確認されます。

これらのテストは、新しいインターフェース実装が期待通りに機能し、既存のString()ParseIP()との整合性が保たれていることを保証します。

コアとなるコードの変更箇所

このコミットによる主要なコード変更は以下の2つのファイルに集中しています。

  1. src/pkg/net/ip.go:

    • import "errors" が追加されました。これはMarshalTextメソッド内でerrors.Newを使用するためです。
    • IP型にMarshalText()メソッドが追加されました。
    • IP型にUnmarshalText()メソッドが追加されました。
  2. src/pkg/net/ip_test.go:

    • TestParseIP関数内にIP.UnmarshalTextのテストケースが追加されました。
    • ipStringTests変数に{IPv4(192, 168, 0, 1), "192.168.0.1"}というテストデータが追加されました。これは、MarshalTextのテストでIPv4アドレスのケースをカバーするためです。
    • TestIPString関数内にIP.MarshalTextのテストケースが追加されました。

コアとなるコードの解説

src/pkg/net/ip.go

package net

import "errors" // errorsパッケージのインポート

// ... (既存のコード) ...

// MarshalText implements the encoding.TextMarshaler interface.
// The encoding is the same as returned by String.
func (ip IP) MarshalText() ([]byte, error) {
	// IPアドレスのバイト長がIPv4(4バイト)でもIPv6(16バイト)でもない場合、
	// 無効なIPアドレスとしてエラーを返す。
	if len(ip) != IPv4len && len(ip) != IPv6len {
		return nil, errors.New("invalid IP address")
	}
	// 有効なIPアドレスの場合、既存のString()メソッドで文字列化し、
	// それをバイトスライスに変換して返す。エラーはnil。
	return []byte(ip.String()), nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The IP address is expected in a form accepted by ParseIP.
func (ip *IP) UnmarshalText(text []byte) error {
	// 入力されたバイトスライスを文字列に変換。
	s := string(text)
	// net.ParseIP関数を使って文字列をIPアドレスにパース。
	x := ParseIP(s)
	// パース結果がnil(無効なIPアドレス文字列)の場合、
	// ParseErrorを返してエラーを通知。
	if x == nil {
		return &ParseError{"IP address", s}
	}
	// パースが成功した場合、パースされたIPアドレスをレシーバのポインタが指す変数に代入。
	*ip = x
	return nil
}

// ... (既存のコード) ...

src/pkg/net/ip_test.go

package net

import (
	"reflect" // reflectパッケージのインポート(DeepEqualのため)
	"testing" // testingパッケージのインポート
)

// ... (既存のテストコード) ...

func TestParseIP(t *testing.T) {
	// ... (既存のテストループ) ...

	// UnmarshalTextのテストを追加
	// 各テストケースの入力文字列 (tt.in) を使ってUnmarshalTextを呼び出す
	var out IP // UnmarshalTextの出力先となるIP変数
	// UnmarshalTextを呼び出し、その結果 (out) が期待値 (tt.out) と一致するか、
	// およびエラーの有無が期待通りかを確認する。
	// (tt.out == nil) != (err != nil) は、期待値がnilならエラーがあるべき、
	// 期待値がnilでないならエラーがないべき、という条件。
	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)
	}
	// ... (既存のテストループの続き) ...
}

// ... (既存のテストコード) ...

var ipStringTests = []struct {
	in  IP
	out string
}{
	// ... (既存のテストデータ) ...
	// IPv4アドレスのテストケースを追加
	{IPv4(192, 168, 0, 1), "192.168.0.1"},
	{nil, "<nil>"},
}

func TestIPString(t *testing.T) {
	// ... (既存のテストループ) ...

	// MarshalTextのテストを追加
	if tt.in != nil { // 入力IPがnilでない場合
		// MarshalTextを呼び出し、その出力 (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)
		}
	} else { // 入力IPがnilの場合
		// MarshalTextを呼び出し、エラーが発生することを確認する。
		if _, err := tt.in.MarshalText(); err == nil {
			t.Errorf("IP.MarshalText(nil) succeeded, want failure")
		}
	}
	// ... (既存のテストループの続き) ...
}

// ... (既存のテストコード) ...

関連リンク

参考にした情報源リンク