[インデックス 1474] ファイルの概要
このコミットは、Go言語のコンパイラ(特に6g)におけるインターフェースのハンドリングに関するバグを特定し、修正するために追加されたテストケースtest/interface6.goの導入を記録しています。このテストは、インターフェースがポインタと値の両方で、かつオブジェクトのサイズ(大小)に関わらず正しく動作するかどうかを検証することを目的としています。コミットメッセージによると、当時6gコンパイラで一つのケースが失敗していたとのことです。
コミット
Go言語のインターフェースが、値レシーバとポインタレシーバを持つメソッド、および異なるサイズの構造体に対して正しく機能するかを検証する新しいテストファイルtest/interface6.goを追加しました。このテストは、当時の6gコンパイラで発生していたインターフェース関連のバグを特定し、修正を促すためのものです。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/94146819729000c50caa89015e458abed7405b77
元コミット内容
commit 94146819729000c50caa89015e458abed7405b77
Author: Ian Lance Taylor <iant@golang.org>
Date: Thu Jan 15 10:15:34 2009 -0800
Test that interfaces are correctly handled by pointer and by
value for large and small objects. Currently one case fails
with 6g.
R=rsc
DELTA=150 (150 added, 0 deleted, 0 changed)
OCL=22823
CL=22827
---
test/golden.out | 4 ++
test/interface6.go | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 154 insertions(+)
diff --git a/test/golden.out b/test/golden.out
index 132286615f..a74d96bcd4 100644
--- test/golden.out
+++ test/golden.out
@@ -34,6 +34,10 @@ Faulting address: 0x0
pc: xxx
+=========== ./interface6.go
+failure in f4 i
+BUG interface6
+
=========== ./peano.go
0! = 1
1! = 1
diff --git a/test/interface6.go b/test/interface6.go
new file mode 100644
index 0000000000..b4a14fb943
--- /dev/null
+++ test/interface6.go
@@ -0,0 +1,150 @@
+// $G $D/$F.go && $L $F.$A && ./$A.out || echo BUG interface6
+//
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+//
+package main
+
+var fail int
+
+func check(b bool, msg string) {
+ if (!b) {
+ println("failure in", msg);
+ fail++;
+ }
+}
+
+type I1 interface { Get() int; Put(int); }
+
+type S1 struct { i int }
+func (p S1) Get() int { return p.i }
+func (p S1) Put(i int) { p.i = i }
+
+func f1() {
+ s := S1{1};
+ var i I1 = s;
+ i.Put(2);
+ check(i.Get() == 1, "f1 i");
+ check(s.i == 1, "f1 s");
+}
+
+func f2() {
+ s := S1{1};
+ var i I1 = &s;
+ i.Put(2);
+ check(i.Get() == 1, "f2 i");
+ check(s.i == 1, "f2 s");
+}
+
+func f3() {
+ s := &S1{1};
+ var i I1 = s;
+ i.Put(2);
+ check(i.Get() == 1, "f3 i");
+ check(s.i == 1, "f3 s");
+}
+
+type S2 struct { i int }
+func (p *S2) Get() int { return p.i }
+func (p *S2) Put(i int) { p.i = i }
+
+func f4() {
+ s := S2{1};
+ var i I1 = s;
+ i.Put(2);
+ check(i.Get() == 2, "f4 i");
+ check(s.i == 1, "f4 s");
+}
+
+func f5() {
+ s := S2{1};
+ var i I1 = &s;
+ i.Put(2);
+ check(i.Get() == 2, "f5 i");
+ check(s.i == 2, "f5 s");
+}
+
+func f6() {
+ s := &S2{1};
+ var i I1 = s;
+ i.Put(2);
+ check(i.Get() == 2, "f6 i");
+ check(s.i == 2, "f6 s");
+}
+
+type I2 interface { Get() int64; Put(int64); }
+
+type S3 struct { i, j, k, l int64 }
+func (p S3) Get() int64 { return p.l }
+func (p S3) Put(i int64) { p.l = i }
+
+func f7() {
+ s := S3{1, 2, 3, 4};
+ var i I2 = s;
+ i.Put(5);
+ check(i.Get() == 4, "f7 i");
+ check(s.l == 4, "f7 s");
+}
+
+func f8() {
+ s := S3{1, 2, 3, 4};
+ var i I2 = &s;
+ i.Put(5);
+ check(i.Get() == 4, "f8 i");
+ check(s.l == 4, "f8 s");
+}
+
+func f9() {
+ s := &S3{1, 2, 3, 4};
+ var i I2 = s;
+ i.Put(5);
+ check(i.Get() == 4, "f9 i");
+ check(s.l == 4, "f9 s");
+}
+
+type S4 struct { i, j, k, l int64 }
+func (p *S4) Get() int64 { return p.l }
+func (p *S4) Put(i int64) { p.l = i }
+
+func f10() {
+ s := S4{1, 2, 3, 4};
+ var i I2 = s;
+ i.Put(5);
+ check(i.Get() == 5, "f10 i");
+ check(s.l == 4, "f10 s");
+}
+
+func f11() {
+ s := S4{1, 2, 3, 4};
+ var i I2 = &s;
+ i.Put(5);
+ check(i.Get() == 5, "f11 i");
+ check(s.l == 5, "f11 s");
+}
+
+func f12() {
+ s := &S4{1, 2, 3, 4};
+ var i I2 = s;
+ i.Put(5);
+ check(i.Get() == 5, "f12 i");
+ check(s.l == 5, "f12 s");
+}
+
+func main() {
+ f1();
+ f2();
+ f3();
+ f4();
+ f5();
+ f6();
+ f7();
+ f8();
+ f9();
+ f10();
+ f11();
+ f12();
+ if fail > 0 {
+ sys.exit(1)
+ }
+}
変更の背景
このコミットが作成された2009年当時、Go言語はまだ開発の初期段階にありました。特にコンパイラ(6gはx86-64アーキテクチャ向けのGoコンパイラ)は成熟しておらず、様々なバグや未実装の機能が存在していました。
コミットメッセージにある「Currently one case fails with 6g.」という記述は、当時の6gコンパイラがインターフェースの特定の挙動、特に値レシーバとポインタレシーバの組み合わせ、または異なるサイズの構造体("large and small objects")をインターフェースに代入した場合に、期待通りに動作しないバグを抱えていたことを示唆しています。
このバグは、Goの型システムとメモリモデルの根幹に関わるものであり、インターフェースの正しい動作はGoのポリモーフィズムを実現する上で不可欠です。そのため、この問題を特定し、修正を促すための厳密なテストケースが必要とされました。test/interface6.goは、まさにその目的のために作成されたものです。このテストが失敗することで、コンパイラの開発者は問題の箇所を特定し、修正に取り組むことができました。
前提知識の解説
Go言語のインターフェース
Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。インターフェース型は、そのインターフェースで定義されたすべてのメソッドを実装する任意の具象型によって満たされます。Goのインターフェースは「暗黙的」であり、JavaやC#のようにimplementsキーワードを使って明示的にインターフェースを実装する必要はありません。型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを満たしているとみなされます。
値レシーバとポインタレシーバ
Goのメソッドは、レシーバ(メソッドが関連付けられる型)を持つ関数です。レシーバには「値レシーバ」と「ポインタレシーバ」の2種類があります。
-
値レシーバ (Value Receiver):
func (s MyStruct) MyMethod() { ... }のように定義されます。- メソッドが呼び出される際、レシーバの値のコピーがメソッドに渡されます。
- メソッド内でレシーバのフィールドを変更しても、元の値には影響しません。これは、メソッドがコピーに対して操作を行うためです。
- 値レシーバを持つメソッドは、値とポインタの両方の変数から呼び出すことができます。Goコンパイラが自動的にポインタをデリファレンスして値のコピーを生成します。
-
ポインタレシーバ (Pointer Receiver):
func (s *MyStruct) MyMethod() { ... }のように定義されます。- メソッドが呼び出される際、レシーバのポインタのコピーがメソッドに渡されます。
- メソッド内でレシーバのフィールドを変更すると、元の値も変更されます。これは、メソッドが元の値が格納されているメモリ位置を直接操作するためです。
- ポインタレシーバを持つメソッドは、ポインタ変数からのみ直接呼び出すことができます。値変数から呼び出す場合、Goコンパイラが自動的にアドレスを取得してポインタを生成します。
インターフェースとレシーバの規則
Goのインターフェースを満たすためには、以下の規則が重要です。
- 値レシーバを持つメソッド:
T型が値レシーバを持つメソッドMを実装している場合、T型と*T型の両方がMを含むインターフェースを満たします。これは、*TからTへのデリファレンスが安全に行えるためです。 - ポインタレシーバを持つメソッド:
T型がポインタレシーバを持つメソッドMを実装している場合、*T型のみがMを含むインターフェースを満たします。T型はインターフェースを満たしません。これは、Tから*Tへのアドレス取得は可能ですが、インターフェースに格納される値がコピーであるため、そのコピーのアドレスを取っても元の値には影響しないというセマンティクス上の問題があるためです。インターフェースに値が格納されている場合、その値のメソッドを呼び出す際にポインタレシーバが必要な場合、Goは一時的なポインタを作成しますが、そのポインタを介した変更は元の値には反映されません。
"Large and Small Objects" の意味
Go言語の文脈で「大きなオブジェクト」と「小さなオブジェクト」という表現は、主にメモリ上での構造体のサイズを指します。
- 小さなオブジェクト:
intのような単一のプリミティブ型や、少数のフィールドを持つ小さな構造体。これらは通常、レジスタに収まるか、スタック上で効率的にコピーできます。 - 大きなオブジェクト: 多数のフィールドを持つ構造体や、配列を含む構造体など、メモリ上で比較的大きな領域を占めるもの。これらを値渡しでコピーすると、パフォーマンスオーバーヘッドが大きくなる可能性があります。
コンパイラは、これらのオブジェクトのサイズに応じて、インターフェースへの値の格納方法やメソッド呼び出しのメカニズムを最適化しようとします。この最適化の過程で、特に初期のコンパイラでは、値レシーバとポインタレシーバの組み合わせや、インターフェースへの代入時にバグが発生する可能性がありました。このコミットのテストは、これらの異なるシナリオを網羅的に検証することで、コンパイラの堅牢性を高めることを目的としています。
技術的詳細
test/interface6.goは、Goのインターフェースとレシーバの挙動を徹底的にテストするために設計されたファイルです。特に、値レシーバとポインタレシーバの組み合わせ、および構造体のサイズがインターフェースの動作に与える影響に焦点を当てています。
テストは、check関数と、f1からf12までの12個のテスト関数で構成されています。
check関数
func check(b bool, msg string) {
if (!b) {
println("failure in", msg);
fail++;
}
}
このヘルパー関数は、テストの各アサーションで使用されます。bがfalseの場合、エラーメッセージmsgを出力し、グローバル変数failをインクリメントします。main関数でfailが0より大きい場合、プログラムは終了コード1で終了し、テストの失敗を示します。
インターフェース I1 と I2
I1:Get() intとPut(int)メソッドを持つインターフェース。int型の値を扱う。I2:Get() int64とPut(int64)メソッドを持つインターフェース。int64型の値を扱う。これは、S3とS4が複数のint64フィールドを持つため、より大きな構造体をシミュレートするために使用されます。
構造体 S1, S2, S3, S4
これらの構造体は、異なるレシーバタイプとサイズをテストするために設計されています。
-
S1(Small, Value Receiver):type S1 struct { i int } func (p S1) Get() int { return p.i } func (p S1) Put(i int) { p.i = i }int型の単一フィールドを持つ小さな構造体で、GetとPutメソッドは値レシーバで定義されています。 -
S2(Small, Pointer Receiver):type S2 struct { i int } func (p *S2) Get() int { return p.i } func (p *S2) Put(i int) { p.i = i }int型の単一フィールドを持つ小さな構造体で、GetとPutメソッドはポインタレシーバで定義されています。 -
S3(Large, Value Receiver):type S3 struct { i, j, k, l int64 } func (p S3) Get() int64 { return p.l } func (p S3) Put(i int64) { p.l = i }複数の
int64フィールドを持つ比較的大きな構造体で、GetとPutメソッドは値レシーバで定義されています。 -
S4(Large, Pointer Receiver):type S4 struct { i, j, k, l int64 } func (p *S4) Get() int64 { return p.l } func (p *S4) Put(i int64) { p.l = i }複数の
int64フィールドを持つ比較的大きな構造体で、GetとPutメソッドはポインタレシーバで定義されています。
テスト関数 f1 から f12
各テスト関数は、特定の構造体とレシーバの組み合わせでインターフェースへの代入とメソッド呼び出しを行い、期待される結果をcheck関数で検証します。特にPutメソッドの呼び出しが、元の構造体の値に影響を与えるかどうか(値渡しによるコピーか、ポインタ渡しによる直接変更か)が重要なポイントです。
-
f1,f2,f3(S1 - Small, Value Receiver):S1は値レシーバを持つため、I1インターフェースにS1の値または*S1のポインタを代入しても、インターフェースを介したPut呼び出しは常にS1のコピーに対して行われます。したがって、元のs.iの値は変更されません。f1():s := S1{1}; var i I1 = s; i.Put(2);->i.Get()は1、s.iは1。f2():s := S1{1}; var i I1 = &s; i.Put(2);->i.Get()は1、s.iは1。f3():s := &S1{1}; var i I1 = s; i.Put(2);->i.Get()は1、s.iは1。
-
f4,f5,f6(S2 - Small, Pointer Receiver):S2はポインタレシーバを持つため、I1インターフェースにS2の値を代入した場合(f4)、S2はI1を満たしません。しかし、Goのコンパイラは、インターフェースに値が格納されている場合でも、その値のアドレスを取得してポインタレシーバのメソッドを呼び出すことができます。ただし、この場合、インターフェースに格納されているのは値のコピーであるため、Putメソッドによる変更は元のsには反映されません。f4():s := S2{1}; var i I1 = s; i.Put(2);->i.Get()は2(インターフェース内のコピーが変更)、s.iは1(元の値は変更されない)。このケースがコミットメッセージで言及されている「Currently one case fails with 6g.」のバグの原因だった可能性が高いです。f5():s := S2{1}; var i I1 = &s; i.Put(2);->i.Get()は2、s.iは2。&sをインターフェースに代入すると、インターフェースはsへのポインタを保持するため、Putによる変更は元のsに反映されます。f6():s := &S2{1}; var i I1 = s; i.Put(2);->i.Get()は2、s.iは2。f5と同様。
-
f7,f8,f9(S3 - Large, Value Receiver):S3は値レシーバを持つため、S1と同様に、インターフェースを介したPut呼び出しは常にS3のコピーに対して行われます。元のs.lの値は変更されません。f7():s := S3{1, 2, 3, 4}; var i I2 = s; i.Put(5);->i.Get()は4、s.lは4。f8():s := S3{1, 2, 3, 4}; var i I2 = &s; i.Put(5);->i.Get()は4、s.lは4。f9():s := &S3{1, 2, 3, 4}; var i I2 = s; i.Put(5);->i.Get()は4、s.lは4。
-
f10,f11,f12(S4 - Large, Pointer Receiver):S4はポインタレシーバを持つため、S2と同様の挙動を示します。f10():s := S4{1, 2, 3, 4}; var i I2 = s; i.Put(5);->i.Get()は5、s.lは4。f4と同様に、インターフェース内のコピーが変更され、元の値は変更されません。f11():s := S4{1, 2, 3, 4}; var i I2 = &s; i.Put(5);->i.Get()は5、s.lは5。f12():s := &S4{1, 2, 3, 4}; var i I2 = s; i.Put(5);->i.Get()は5、s.lは5。
このテストスイートは、Goのインターフェースとレシーバのセマンティクスに関する深い理解を必要とし、コンパイラがこれらの複雑なケースを正しく処理できることを保証するための重要な役割を果たします。
コアとなるコードの変更箇所
このコミットによる変更は主に2つのファイルにあります。
-
test/golden.out:- このファイルは、Goのテストスイートにおける期待される出力(ゴールデンファイル)を定義しています。
test/interface6.goが追加されたことにより、そのテストの実行結果がgolden.outに追加されました。- 追加された行:
これは、=========== ./interface6.go failure in f4 i BUG interface6interface6.goのテストが実行された際に、f4 iというメッセージで失敗し、最終的にBUG interface6というメッセージが出力されることを期待していることを示しています。これは、このコミットが修正しようとしているバグの存在を明示的に示しています。
-
test/interface6.go:- このファイルは新規に追加されたテストケースです。
- Goのインターフェース、値レシーバ、ポインタレシーバ、および異なるサイズの構造体(
S1,S2,S3,S4)の組み合わせを網羅的にテストするコードが含まれています。 main関数内でf1からf12までの各テスト関数を呼び出し、fail変数の値に基づいてテストの成否を判断します。- ファイルの冒頭には、テストの実行方法と期待される結果を示すコメント行があります:
// $G $D/$F.go && $L $F.$A && ./$A.out || echo BUG interface6。これは、コンパイルと実行が失敗した場合にBUG interface6というメッセージを出力することを意味し、テストがバグを検出したことを示します。
コアとなるコードの解説
test/interface6.goの主要な部分は、Goのインターフェースの挙動を様々なシナリオで検証する一連のテスト関数です。
check 関数
func check(b bool, msg string) {
if (!b) {
println("failure in", msg);
fail++;
}
}
これはテストのアサーションを行うためのユーティリティ関数です。bがfalseの場合(つまりテストが失敗した場合)、指定されたmsgを出力し、グローバルカウンタfailをインクリメントします。main関数はfailの値を見て、テストスイート全体が成功したか失敗したかを判断します。
インターフェース I1 と I2
type I1 interface { Get() int; Put(int); }
type I2 interface { Get() int64; Put(int64); }
これらはテスト対象のインターフェースです。I1はint型を扱い、I2はint64型を扱います。I2は、より大きなデータ型を扱うことで、構造体のサイズがインターフェースの挙動に影響を与える可能性をテストします。
構造体 S1, S2, S3, S4 とそのメソッド
これらの構造体は、レシーバのタイプ(値またはポインタ)と構造体のサイズ(小さいまたは大きい)の組み合わせを網羅するために設計されています。
-
S1(値レシーバ, 小さい構造体):type S1 struct { i int } func (p S1) Get() int { return p.i } func (p S1) Put(i int) { p.i = i }Putメソッドが値レシーバであるため、p.i = iの変更はpのコピーに対して行われ、元のS1インスタンスには影響しません。 -
S2(ポインタレシーバ, 小さい構造体):type S2 struct { i int } func (p *S2) Get() int { return p.i } func (p *S2) Put(i int) { p.i = i }Putメソッドがポインタレシーバであるため、p.i = iの変更は元のS2インスタンスに直接影響します。 -
S3(値レシーバ, 大きい構造体):type S3 struct { i, j, k, l int64 } func (p S3) Get() int64 { return p.l } func (p S3) Put(i int64) { p.l = i }S1と同様に、Putメソッドは値レシーバであり、元のS3インスタンスには影響しません。 -
S4(ポインタレシーバ, 大きい構造体):type S4 struct { i, j, k, l int64 } func (p *S4) Get() int64 { return p.l } func (p *S4) Put(i int64) { p.l = i }S2と同様に、Putメソッドはポインタレシーバであり、元のS4インスタンスに直接影響します。
テスト関数 f1 から f12 のパターン
各fX関数は、以下の3つの主要なパターンをテストしています。
- 具象型の値をインターフェースに代入:
var i I1 = s; - 具象型へのポインタをインターフェースに代入:
var i I1 = &s; - 具象型へのポインタを生成し、それをインターフェースに代入:
s := &S1{1}; var i I1 = s;
これらのパターンと、各構造体のレシーバタイプ(値 vs ポインタ)の組み合わせにより、Goのインターフェースの挙動に関する様々なエッジケースが検証されます。
特に注目すべきは、f4()とf10()のケースです。
f4():s := S2{1}; var i I1 = s; i.Put(2);S2はポインタレシーバを持つPutメソッドを実装しています。sは値型ですが、Goはインターフェースに値を代入した場合でも、その値のコピーに対してポインタレシーバのメソッドを呼び出すことができます。しかし、このPut呼び出しはインターフェースに格納されたsのコピーに対して行われるため、元のs.iは変更されません。 期待される結果はcheck(i.Get() == 2, "f4 i"); check(s.i == 1, "f4 s");です。golden.outにfailure in f4 iとあることから、このi.Get() == 2の部分が当時の6gコンパイラで正しく評価されていなかった(おそらくi.Get()が1を返していた)ことが示唆されます。これは、インターフェースが値型を保持している場合のポインタレシーバメソッドの呼び出しにおける、コンパイラのバグを示しています。
このテストファイル全体が、Goのインターフェースのセマンティクス、特に値とポインタの挙動がコンパイラによって正しく実装されていることを保証するための重要な回帰テストとして機能します。
関連リンク
- Go言語の仕様: https://go.dev/ref/spec (特に "Method sets" と "Interface types" のセクション)
- Go Blog: The Go Programming Language: https://go.dev/blog/ (Goの設計思想や特定の機能に関する記事が見つかる可能性があります)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のインターフェースとメソッドセットに関する一般的な知識
- Go言語のコンパイラ(
6gなど)に関する歴史的背景(Goの初期開発段階の文脈) - Go言語における値レシーバとポインタレシーバのセマンティクスに関する情報