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

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

このコミットは、Goコンパイラ(cmd/gc)における、アンダースコア(_)フィールドを持つ構造体の比較に関するバグ修正を扱っています。具体的には、構造体内にブランク識別子(_)として宣言されたフィールドが存在する場合に、その構造体の比較やハッシュ計算が正しく行われない問題を解決します。

コミット

commit c4c92ebeb656d059f88a2164f6ca9b136ce9fbf9
Author: Russ Cox <rsc@golang.org>
Date:   Fri Feb 17 14:45:29 2012 -0500

    cmd/gc: fix comparison of struct with _ field
    
    Fixes #2989.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5674091

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

https://github.com/golang/go/commit/c4c92ebeb656d059f88a2164f6ca9b136ce9fbf9

元コミット内容

cmd/gc: アンダースコアフィールドを持つ構造体の比較を修正。 Issue #2989 を修正。

変更の背景

Go言語において、構造体のフィールド名にアンダースコア(_)を使用すると、そのフィールドは「ブランク識別子(blank identifier)」として扱われます。これは、そのフィールドの値が使用されないことをコンパイラに伝えるための特別な構文です。通常、ブランク識別子で宣言された変数は、その値が破棄されるため、比較やハッシュ計算の対象から除外されるべきです。

このコミット以前のGoコンパイラでは、構造体内にブランク識別子フィールドが存在する場合、その構造体の比較(==!=)やハッシュ計算(マップのキーとして使用される場合など)が正しく行われないというバグが存在していました。具体的には、ブランク識別子フィールドが比較やハッシュ計算の対象に含まれてしまい、予期せぬ結果を引き起こす可能性がありました。

この問題は、Goの型システムとコンパイラの内部処理における、ブランク識別子フィールドの取り扱いに関する不整合に起因していました。特に、構造体の比較可能性(comparability)を判断するロジックや、実際に比較・ハッシュ計算を行うコード生成ロジックが、ブランク識別子フィールドを適切に無視していなかったと考えられます。

前提知識の解説

  • Go言語の構造体(Structs): 複数の異なる型のフィールドをまとめた複合データ型です。
  • ブランク識別子(Blank Identifier _: Go言語の特別な識別子で、値が使用されないことを明示的に示すために使われます。例えば、関数の戻り値の一部を無視したり、インポートしたパッケージの副作用だけを利用したい場合などに使用されます。構造体のフィールド名に_を使用すると、そのフィールドはブランク識別子として扱われ、そのフィールドの値は通常の変数のようにアクセスしたり使用したりすることはできません。
  • 構造体の比較可能性(Comparability): Go言語では、すべての型の値が比較可能ではありません。構造体が比較可能であるためには、そのすべてのフィールドが比較可能である必要があります。また、スライス、マップ、関数などの型は比較不可能です。
  • ハッシュ計算(Hashing): マップのキーとして使用される型は、ハッシュ可能である必要があります。ハッシュ計算は、値から一意のハッシュ値を生成するプロセスです。構造体がハッシュ可能であるためには、そのすべてのフィールドがハッシュ可能である必要があります。
  • Goコンパイラ(cmd/gc: Go言語のソースコードを機械語に変換する主要なコンパイラです。型チェック、コード生成、最適化などの様々な段階を含みます。
  • algtype1: コンパイラ内部で型の比較可能性を判断する関数の一つです。AMEMはメモリ比較可能であることを示します。
  • genhash: コンパイラ内部で型のハッシュ関数を生成する関数です。
  • geneq: コンパイラ内部で型の等価性比較関数を生成する関数です。

技術的詳細

このコミットの主要な変更点は、Goコンパイラの型システムとコード生成部分において、ブランク識別子フィールドを適切に無視するように修正したことです。

  1. isblanksym 関数の導入: src/cmd/gc/go.hisblanksym(Sym *s) 関数が追加されました。この関数は、与えられたシンボル(Sym)がブランク識別子(_)を表すかどうかを判定します。具体的には、シンボルの名前が単一のアンダースコアであるかどうかをチェックします。 src/cmd/gc/subr.c では、既存の isblank(Node *n) 関数が isblanksym を呼び出すように変更され、さらに isblanksym 自体の実装が追加されました。これにより、ノード(ASTの要素)やシンボルがブランク識別子であるかを統一的に判定できるようになりました。

  2. algtype1 におけるブランクフィールドの無視: src/cmd/gc/subr.calgtype1 関数は、構造体のフィールドを走査してその比較可能性を判断します。この関数内で、構造体のフィールドをイテレートする際に、isblanksym(t1->sym) をチェックし、もしフィールドがブランク識別子であれば continue して次のフィールドに進むように変更されました。これにより、ブランクフィールドが構造体の比較可能性の判断に影響を与えないようになりました。

  3. genhash におけるブランクフィールドの無視: src/cmd/gc/subr.cgenhash 関数は、構造体のハッシュ関数を生成します。この関数内で、メモリ比較可能なフィールドのブロックを特定する際に、isblanksym(t1->sym) または algtype1(t1->type, nil) == AMEM のいずれかを満たすフィールドを対象とするように条件が変更されました。さらに、memhash を実行する前に、first ポインタがブランク識別子フィールドをスキップするように while(first != T && isblanksym(first->sym)) ループが追加されました。これにより、ブランクフィールドがハッシュ計算の対象から除外されるようになりました。

  4. geneq におけるブランクフィールドの無視: src/cmd/gc/subr.cgeneq 関数は、構造体の等価性比較関数を生成します。genhash と同様に、isblanksym(t1->sym) または algtype1(t1->type, nil) == AMEM のいずれかを満たすフィールドを対象とするように条件が変更されました。また、memequal を実行する前に、first ポインタがブランク識別子フィールドをスキップするように while(first != T && isblanksym(first->sym)) ループが追加されました。さらに、複数のフィールドをまとめて比較するロジックにおいて、ブランクフィールドが eqfield の呼び出しに含まれないように if(!isblanksym(first->sym)) のチェックが追加されました。これにより、ブランクフィールドが構造体の比較から除外されるようになりました。

これらの変更により、Goコンパイラは構造体内のブランク識別子フィールドを正しく認識し、それらを比較やハッシュ計算の対象から除外するようになりました。

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

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -1172,6 +1172,7 @@ int
 	implements(Type *t, Type *iface, Type **missing, Type **have, int *ptr);\n void\timportdot(Pkg *opkg, Node *pack);\n int\tis64(Type *t);\n int\tisblank(Node *n);\
+int\tisblanksym(Sym *s);\
 int\tisfixedarray(Type *t);\
 int\tisideal(Type *t);\
 int\tisinter(Type *t);

isblanksym 関数のプロトタイプ宣言が追加されました。

src/cmd/gc/subr.c

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -571,6 +571,8 @@ algtype1(Type *t, Type **bad)
 		}
 		ret = AMEM;
 		for(t1=t->type; t1!=T; t1=t1->down) {
+			if(isblanksym(t1->sym))
+				continue;
 			a = algtype1(t1->type, bad);
 			if(a == ANOEQ)
 				return ANOEQ;  // not comparable
@@ -887,12 +889,20 @@ isslice(Type *t)
 
 int
 isblank(Node *n)
+{
+	if(n == N)
+		return 0;
+	return isblanksym(n->sym);
+}
+
+int
+isblanksym(Sym *s)
 {
 	char *p;
 
-	if(n == N || n->sym == S)
+	if(s == S)
 		return 0;
-	p = n->sym->name;
+	p = s->name;
 	if(p == nil)
 		return 0;
 	return p[0] == '_' && p[1] == '\0';
@@ -2652,12 +2662,14 @@ genhash(Sym *sym, Type *t)
 		// and calling specific hash functions for the others.
 		first = T;
 		for(t1=t->type;; t1=t1->down) {
-			if(t1 != T && algtype1(t1->type, nil) == AMEM) {
+			if(t1 != T && (isblanksym(t1->sym) || algtype1(t1->type, nil) == AMEM)) {
 				if(first == T)
 					first = t1;
 				continue;
 			}
 			// Run memhash for fields up to this one.
+			while(first != T && isblanksym(first->sym))
+				first = first->down;
 			if(first != T) {
 				if(first->down == t1)
 					size = first->type->width;
@@ -2867,7 +2879,7 @@ geneq(Sym *sym, Type *t)
 		// and calling specific equality tests for the others.
 		first = T;
 		for(t1=t->type;; t1=t1->down) {
-			if(t1 != T && algtype1(t1->type, nil) == AMEM) {
+			if(t1 != T && (isblanksym(t1->sym) || algtype1(t1->type, nil) == AMEM)) {
 				if(first == T)
 					first = t1;
 				continue;
@@ -2875,13 +2887,16 @@ geneq(Sym *sym, Type *t)
 			// Run memequal for fields up to this one.
 			// TODO(rsc): All the calls to newname are wrong for
 			// cross-package unexported fields.
+			while(first != T && isblanksym(first->sym))
+				first = first->down;
 			if(first != T) {
 				if(first->down == t1) {
 					fn->nbody = list(fn->nbody, eqfield(np, nq, newname(first->sym), neq));
 				} else if(first->down->down == t1) {
 					fn->nbody = list(fn->nbody, eqfield(np, nq, newname(first->sym), neq));
 					first = first->down;
-					fn->nbody = list(fn->nbody, eqfield(np, nq, newname(first->sym), neq));
+					if(!isblanksym(first->sym))
+						fn->nbody = list(fn->nbody, eqfield(np, nq, newname(first->sym), neq));
 				} else {
 					// More than two fields: use memequal.
 					if(t1 == T)

isblanksym の実装と、algtype1, genhash, geneq 関数におけるブランクフィールドの処理ロジックが変更されました。

test/cmp.go

--- a/test/cmp.go
+++ b/test/cmp.go
@@ -281,6 +281,25 @@ func main() {
 		isfalse(ix != z)
 		isfalse(iz != x)
 	}
+	
+	// structs with _ fields
+	{
+		var x = struct {
+			x int
+			_ []int
+			y float64
+			_ float64
+			z int
+		}{
+			x: 1, y: 2, z: 3,
+		}
+		var ix interface{} = x
+		
+		istrue(x == x)
+		istrue(x == ix)
+		istrue(ix == x)
+		istrue(ix == ix)
+	}
 
 	// arrays
 	{

ブランクフィールドを持つ構造体の比較が正しく行われることを検証するための新しいテストケースが追加されました。このテストケースでは、_ []int_ float64 というブランクフィールドを持つ構造体を定義し、その構造体自身やインターフェース型との比較が期待通りに true になることを確認しています。

test/cmp6.go

--- a/test/cmp6.go
+++ b/test/cmp6.go
@@ -15,6 +15,10 @@ type T3 struct{ z []int }\n \n var t3 T3\n \n+type T4 struct { _ []int; a float64 }\n+\n+var t4 T4\n+\n func main() {\n 	// Arguments to comparison must be\n 	// assignable one to the other (or vice versa)\n@@ -46,6 +50,7 @@ func main() {\n \n 	// Comparison of structs should have a good message\n 	use(t3 == t3) // ERROR "struct|expected"\n+\tuse(t4 == t4) // ok; the []int is a blank field\n \n 	// Slices, functions, and maps too.\n 	var x []int

T4というブランクフィールドを持つ構造体が追加され、その比較がエラーにならないことを確認するテストケースが追加されました。コメント // ok; the []int is a blank field は、ブランクフィールドであるため、スライス(比較不可能)が含まれていても構造体全体が比較可能であることを示しています。

コアとなるコードの解説

このコミットの核心は、Goコンパイラが構造体の比較可能性、ハッシュ計算、および等価性比較のコード生成を行う際に、ブランク識別子フィールドを適切に「無視」するロジックを導入した点にあります。

  • isblanksym: このヘルパー関数は、Goのシンボルがブランク識別子(_)であるかを効率的に判定するために導入されました。これにより、コンパイラの他の部分でブランクフィールドのチェックを簡潔に行えるようになりました。

  • algtype1 の修正: 構造体の比較可能性を判断する際に、ブランクフィールドは比較の対象外となるため、algtype1 はこれらのフィールドをスキップするように変更されました。これにより、ブランクフィールドが存在しても、その構造体が比較可能であるかどうかの判断が正しく行われるようになりました。例えば、比較不可能な型(スライスなど)がブランクフィールドとして構造体に含まれていても、その構造体自体は比較可能と判断されるようになります。

  • genhashgeneq の修正: これらの関数は、それぞれ構造体のハッシュ関数と等価性比較関数を生成します。ブランクフィールドは値を持たないため、ハッシュ計算や等価性比較の対象に含めるべきではありません。修正されたロジックでは、フィールドを走査する際にブランクフィールドをスキップし、メモリ比較(memhash, memequal)の対象となるフィールドのブロックを決定する際にも、ブランクフィールドを考慮から外すようになりました。特に geneq では、複数のフィールドをまとめて比較する際に、ブランクフィールドが誤って比較対象に含まれないように明示的なチェックが追加されています。

これらの変更により、Go言語の仕様に沿って、ブランク識別子フィールドが構造体の比較やハッシュ計算に影響を与えないという振る舞いが、コンパイラレベルで保証されるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード
  • Go言語のIssueトラッカー (ただし、Issue #2989は内部的なものであり、公開されている情報は見つかりませんでした。)