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

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

このコミットは、Go言語の標準ライブラリであるnetパッケージのテストコードにおける、WindowsおよびPlan 9ビルドの問題を修正するものです。具体的には、テストの初期化段階でUnix系OS専用の関数が呼び出されることによって発生していたビルドエラーを解消します。

コミット

commit 1fdb3e2ed66dda36f98937f9570d007dbc7300bc
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Mar 13 07:42:55 2013 -0700

    net: fix windows and plan9 build
    
    Don't call unix-only function in test init.
    
    R=golang-dev, alex.brainman
    CC=golang-dev
    https://golang.org/cl/7778043

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

https://github.com/golang/go/commit/1fdb3e2ed66dda36f98937f9570d007dbc7300bc

元コミット内容

net: fix windows and plan9 build Don't call unix-only function in test init.

このコミットは、netパッケージのテストコードがWindowsおよびPlan 9環境でビルドできない問題を修正します。問題の原因は、テストの初期化時にUnix系OS専用の関数が呼び出されていたことにあります。

変更の背景

Go言語はクロスプラットフォーム対応を重視しており、単一のコードベースから様々なOS向けのバイナリを生成できることが大きな特徴です。しかし、特定のOSに依存する機能(例えばUnixドメインソケットなど)を使用する場合、その依存性を適切に管理しないと、非対応OSでのビルドエラーや実行時エラーが発生します。

このコミット以前のnetパッケージのテストコードでは、connTestspacketConnTestsといったテストケースを定義する際に、Unixドメインソケットのアドレスを生成するtestUnixAddr()関数が直接呼び出されていました。Go言語では、グローバル変数や構造体の初期化はプログラムの起動時、特にinit関数が実行される前に行われます。testUnixAddr()関数はUnix系OSのファイルシステムに一時ファイルを作成するなどの操作を行うため、WindowsやPlan 9のようなUnixドメインソケットをサポートしないOSでは、この初期化処理自体が失敗し、結果としてビルドエラーが発生していました。

開発者は、テストコードが特定のOSでのみ動作する関数を、そのOSがサポートされていない環境で初期化時に呼び出すことによって、クロスプラットフォームビルドが妨げられるという問題に直面していました。このコミットは、この問題を解決し、netパッケージのテストがより広範なプラットフォームで実行可能になるようにするためのものです。

前提知識の解説

Go言語のnetパッケージ

Go言語の標準ライブラリであるnetパッケージは、ネットワークI/Oプリミティブを提供します。TCP/IP、UDP、Unixドメインソケットなど、様々なネットワークプロトコルを扱うための機能が含まれています。ソケットの作成、接続、データの送受信、リスナーの構築などが可能です。

Unixドメインソケット (Unix Domain Sockets / UDS)

Unixドメインソケットは、同じホストマシン上のプロセス間通信(IPC)のためのメカニズムです。ネットワークソケット(TCP/IPなど)とは異なり、ファイルシステム上のパス名(例: /tmp/mysocket)をアドレスとして使用します。これにより、ファイルシステム権限によるアクセス制御が可能になり、ネットワークスタックを介さないため、TCP/IPソケットよりも高速な通信が期待できます。ただし、Unix系OS(Linux, macOS, BSDなど)に特有の機能であり、Windowsではネイティブにはサポートされていません(WSLなどを介して利用できる場合を除く)。Plan 9もUnix系OSとは異なる設計思想を持つため、直接的な互換性はありません。

Go言語のinit関数と変数初期化

Go言語では、パッケージの初期化は特別なinit関数によって行われます。init関数は、パッケージ内のすべての変数が宣言され、初期化された後に、main関数が実行される前に自動的に呼び出されます。また、グローバル変数や構造体のフィールドの初期化式は、コンパイル時またはプログラム起動時に評価されます。 この挙動が今回の問題の根源でした。connTestspacketConnTestsのようなグローバルなテストデータ構造体に含まれるフィールドの初期化式が、プログラム起動時に評価されてしまい、その中に含まれるtestUnixAddr()の呼び出しが、まだUnixドメインソケットが利用できない環境で実行されてしまったのです。

クロスプラットフォーム開発における課題

Go言語はビルドタグ(build tags)などを用いて、特定のOSやアーキテクチャに依存するコードを条件付きでコンパイルする仕組みを提供しています。しかし、今回のケースのように、テストデータ構造体の初期化式でOS依存の関数を直接呼び出すと、ビルドタグによる制御が及ばない初期化フェーズで問題が発生する可能性があります。

技術的詳細

このコミットの核心は、テストデータ構造体connTestspacketConnTestsaddrフィールド(およびaddr1, addr2フィールド)の型を変更することにあります。

変更前:

var connTests = []struct {
	net  string
	addr string
}{
	{"tcp", "127.0.0.1:0"},
	{"unix", testUnixAddr()}, // ここでtestUnixAddr()が初期化時に呼び出される
	{"unixpacket", testUnixAddr()}, // ここでtestUnixAddr()が初期化時に呼び出される
}

変更後:

var connTests = []struct {
	net  string
	addr func() string // 型がstringからfunc() stringに変更
}{
	{"tcp", func() string { return "127.0.0.1:0" }}, // リテラルも関数でラップ
	{"unix", testUnixAddr}, // 関数そのものを代入(呼び出しはしない)
	{"unixpacket", testUnixAddr}, // 関数そのものを代入(呼び出しはしない)
}

この変更により、testUnixAddr()関数はテストデータ構造体の初期化時には呼び出されず、その代わりにfunc() string型の関数リテラルまたは関数ポインタがフィールドに格納されます。実際にアドレスが必要になるのは、テストケースが実行され、ListenListenPacketが呼び出される時点です。その時点で初めてtt.addr()(またはtt.addr1(), tt.addr2())が評価され、testUnixAddr()が実行されます。

これにより、WindowsやPlan 9のようなUnixドメインソケットをサポートしない環境では、unixunixpacketのテストケースがスキップされるか、またはListen呼び出し時にエラーとなるだけで、プログラムの初期化段階でビルドが失敗することがなくなります。

また、packetconn_test.goには、文字列リテラルをfunc() string型に変換するためのヘルパー関数strfuncが追加されています。

func strfunc(s string) func() string {
	return func() string {
		return s
	}
}

これは、"127.0.0.1:0"のような固定アドレスもfunc() string型に合わせるために導入されました。

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

このコミットで変更されたファイルは以下の2つです。

  1. src/pkg/net/conn_test.go
  2. src/pkg/net/packetconn_test.go

src/pkg/net/conn_test.go の変更点

  • connTests構造体のaddrフィールドの型がstringからfunc() stringに変更されました。
  • connTestsの初期化において、testUnixAddr()の直接呼び出しがtestUnixAddr(関数ポインタ)に変更されました。
  • "127.0.0.1:0"のような文字列リテラルもfunc() string { return "127.0.0.1:0" }のように関数リテラルでラップされました。
  • Listen関数を呼び出す際に、tt.addrが関数として呼び出されるようにtt.addr()に変更されました。
  • defer closerの引数も、実際に評価されたaddr変数を使用するように変更されました。

src/pkg/net/packetconn_test.go の変更点

  • strfuncヘルパー関数が追加されました。
  • packetConnTests構造体のaddr1addr2フィールドの型がstringからfunc() stringに変更されました。
  • packetConnTestsの初期化において、testUnixAddr()の直接呼び出しがtestUnixAddr(関数ポインタ)に変更されました。
  • "127.0.0.1:0""127.0.0.1"のような文字列リテラルもstrfunc関数でラップされるように変更されました。
  • ListenPacket関数を呼び出す際に、tt.addr1tt.addr2が関数として呼び出されるようにtt.addr1()tt.addr2()に変更されました。
  • defer closerの引数も、実際に評価されたaddr1addr2変数を使用するように変更されました。

コアとなるコードの解説

src/pkg/net/conn_test.go

--- a/src/pkg/net/conn_test.go
+++ b/src/pkg/net/conn_test.go
@@ -16,11 +16,11 @@ import (
 
 var connTests = []struct {
 	net  string
-	addr string
+	addr func() string // 変更点1: addrフィールドの型をfunc() stringに変更
 }{
-	{"tcp", "127.0.0.1:0"},
-	{"unix", testUnixAddr()}, // 変更前: 初期化時に呼び出される
-	{"unixpacket", testUnixAddr()}, // 変更前: 初期化時に呼び出される
+	{"tcp", func() string { return "127.0.0.1:0" }}, // 変更点2: 文字列リテラルも関数でラップ
+	{"unix", testUnixAddr}, // 変更点3: 関数そのものを代入(呼び出しを遅延)
+	{"unixpacket", testUnixAddr}, // 変更点3: 関数そのものを代入(呼び出しを遅延)
 }
 
 // someTimeout is used just to test that net.Conn implementations
@@ -41,12 +41,12 @@ func TestConnAndListener(t *testing.T) {
 			}
 		}
 
-		ln, err := Listen(tt.net, tt.addr) // 変更前: tt.addrはstring
+		addr := tt.addr() // 変更点4: 関数を呼び出してアドレスを取得
+		ln, err := Listen(tt.net, addr) // 変更点5: 取得したアドレスを使用
 		if err != nil {
 			t.Fatalf("Listen failed: %v", err)
 		}
 		defer func(ln Listener, net, addr string) {
 			ln.Close()
 			switch net {
 			case "unix", "unixpacket":
-				os.Remove(addr)
+				os.Remove(addr) // 変更点6: 取得したアドレスを使用
 			}
-		}(ln, tt.net, tt.addr)
+		}(ln, tt.net, addr) // 変更点7: 取得したアドレスをdeferに渡す
 		ln.Addr()
 
 		done := make(chan int)

src/pkg/net/packetconn_test.go

--- a/src/pkg/net/packetconn_test.go
+++ b/src/pkg/net/packetconn_test.go
@@ -15,14 +15,20 @@ import (
 	"time"
 )
 
+// 変更点1: 文字列をfunc() stringに変換するヘルパー関数
+func strfunc(s string) func() string {
+	return func() string {
+		return s
+	}
+}
+
 var packetConnTests = []struct {
 	net   string
-	addr1 string
-	addr2 string
+	addr1 func() string // 変更点2: addr1フィールドの型をfunc() stringに変更
+	addr2 func() string // 変更点3: addr2フィールドの型をfunc() stringに変更
 }{
-	{"udp", "127.0.0.1:0", "127.0.0.1:0"},
-	{"ip:icmp", "127.0.0.1", "127.0.0.1"},
-	{"unixgram", testUnixAddr(), testUnixAddr()},
+	{"udp", strfunc("127.0.0.1:0"), strfunc("127.0.0.1:0")}, // 変更点4: strfuncを使用
+	{"ip:icmp", strfunc("127.0.0.1"), strfunc("127.0.0.1")}, // 変更点5: strfuncを使用
+	{"unixgram", testUnixAddr, testUnixAddr}, // 変更点6: 関数そのものを代入
 }
 
 func TestPacketConn(t *testing.T) {
@@ -70,21 +76,22 @@ func TestPacketConn(t *testing.T) {
 			continue
 		}
 
-		c1, err := ListenPacket(tt.net, tt.addr1) // 変更前: tt.addr1はstring
+		addr1, addr2 := tt.addr1(), tt.addr2() // 変更点7: 関数を呼び出してアドレスを取得
+		c1, err := ListenPacket(tt.net, addr1) // 変更点8: 取得したアドレスを使用
 		if err != nil {
 			t.Fatalf("ListenPacket failed: %v", err)
 		}
-		defer closer(c1, netstr[0], tt.addr1, tt.addr2) // 変更前: tt.addr1, tt.addr2はstring
+		defer closer(c1, netstr[0], addr1, addr2) // 変更点9: 取得したアドレスを使用
 		c1.LocalAddr()
 		c1.SetDeadline(time.Now().Add(100 * time.Millisecond))
 		c1.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
 		c1.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))
 
-		c2, err := ListenPacket(tt.net, tt.addr2) // 変更前: tt.addr2はstring
+		c2, err := ListenPacket(tt.net, addr2) // 変更点10: 取得したアドレスを使用
 		if err != nil {
 			t.Fatalf("ListenPacket failed: %v", err)
 		}
-		defer closer(c2, netstr[0], tt.addr1, tt.addr2) // 変更前: tt.addr1, tt.addr2はstring
+		defer closer(c2, netstr[0], addr1, addr2) // 変更点11: 取得したアドレスを使用
 		c2.LocalAddr()
 		c2.SetDeadline(time.Now().Add(100 * time.Millisecond))
 		c2.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
@@ -152,11 +159,12 @@ func TestConnAndPacketConn(t *testing.T) {
 			continue
 		}
 
-		c1, err := ListenPacket(tt.net, tt.addr1) // 変更前: tt.addr1はstring
+		addr1, addr2 := tt.addr1(), tt.addr2() // 変更点12: 関数を呼び出してアドレスを取得
+		c1, err := ListenPacket(tt.net, addr1) // 変更点13: 取得したアドレスを使用
 		if err != nil {
 			t.Fatalf("ListenPacket failed: %v", err)
 		}
-		defer closer(c1, netstr[0], tt.addr1, tt.addr2) // 変更前: tt.addr1, tt.addr2はstring
+		defer closer(c1, netstr[0], addr1, addr2) // 変更点14: 取得したアドレスを使用
 		c1.LocalAddr()
 		c1.SetDeadline(time.Now().Add(100 * time.Millisecond))
 		c1.SetReadDeadline(time.Now().Add(100 * time.Millisecond))

これらの変更により、testUnixAddr()のようなOS依存の関数が、そのOSがサポートされていない環境でテストデータ構造体の初期化時に呼び出されることがなくなりました。代わりに、アドレスが必要になった時点で初めて関数が実行されるため、クロスプラットフォームでのビルドとテストの互換性が向上しました。

関連リンク

参考にした情報源リンク