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

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

このコミットは、Go言語のコンパイラにおけるバグ修正に関するものです。具体的には、インターフェース型のアサーション f, ok := i.(Foo) が、i が既に Foo と同等である場合にコンパイルエラーとなる問題を解決しています。この修正は、Go言語の初期段階における型システムとコンパイラの挙動の成熟を示す重要な一歩と言えます。

コミット

commit fa615a3b303fdf10e9e3dcd21d372c0ed8e7351a
Author: Rob Pike <r@golang.org>
Date:   Mon Jan 26 18:35:18 2009 -0800

    f, ok := i.(Foo) does not compile if i already is equivalent to Foo
    
    R=rsc
    DELTA=18  (18 added, 0 deleted, 0 changed)
    OCL=23544
    CL=23547
---
 test/bugs/bug135.go | 18 ++++++++++++++++++
 test/golden.out     |  4 ++++\
 2 files changed, 22 insertions(+)

diff --git a/test/bugs/bug135.go b/test/bugs/bug135.go
new file mode 100644
index 0000000000..d7115c4f27
--- /dev/null
+++ b/test/bugs/bug135.go
@@ -0,0 +1,18 @@
+// $G $D/$F.go || echo BUG: should compile
+//
+// 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
+//
+type Foo interface { }
+//
+type T struct {}
+func (t *T) foo() {}
+//
+func main() {
+//  t := new(T);
+//  var i interface {};
+//  f, ok := i.(Foo);
+// }
diff --git a/test/golden.out b/test/golden.out
index d70df181d3..241225ab09 100644
--- a/test/golden.out
+++ b/test/golden.out
@@ -181,6 +181,10 @@ BUG: should not compile
 =========== bugs/bug132.go
 BUG: compilation succeeds incorrectly
 
+=========== bugs/bug135.go
+bugs/bug135.go:13: assignment count mismatch: 2 = 1
+BUG: should compile
+
 =========== fixedbugs/bug016.go
 fixedbugs/bug016.go:7: overflow converting constant to uint
 

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

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

元コミット内容

f, ok := i.(Foo) does not compile if i already is equivalent to Foo

このコミットメッセージは、Go言語の型アサーションに関する特定のバグを簡潔に示しています。i.(Foo) という型アサーションが、変数 i が既に Foo インターフェースと同等である場合にコンパイルエラーになるという問題です。

変更の背景

Go言語は静的型付け言語でありながら、インターフェースを通じて動的な振る舞いを許容します。型アサーションは、インターフェース型の変数が実際に特定の具象型または別のインターフェース型を保持しているかどうかを確認し、その値を取り出すための重要なメカニズムです。

このコミットが作成された2009年1月は、Go言語がまだ一般に公開される前の開発初期段階でした。当時のGoコンパイラはまだ成熟しておらず、型システムに関するエッジケースやバグが多数存在していました。このバグは、インターフェースの内部表現と型アサーションの処理ロジックにおける不整合に起因するものと考えられます。

具体的には、f, ok := i.(Foo) という形式の型アサーションは、iFoo インターフェースを実装しているかどうかをチェックし、もし実装していればその値を f に代入し、成功したかどうかを ok にブール値で返します。このバグは、i が既に Foo インターフェース型である場合に、コンパイラがこのアサーションを正しく処理できず、「assignment count mismatch: 2 = 1」のようなエラーを吐き出していたことを示唆しています。これは、コンパイラが i.(Foo) の結果を単一の値として解釈し、2つの変数(fok)への代入とミスマッチを起こしていた可能性が高いです。

前提知識の解説

このコミットの理解には、以下のGo言語の概念が不可欠です。

  1. インターフェース (Interfaces): Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェースは、JavaやC#のような明示的なimplementsキーワードを必要とせず、型がインターフェースで定義されたすべてのメソッドを実装していれば、そのインターフェースを満たしていると見なされます(構造的型付け)。 例:

    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    

    interface{} は「空のインターフェース」と呼ばれ、いかなる型もメソッドを持たないため、すべての型が interface{} を満たします。これは、任意の型の値を保持できる汎用的なコンテナとしてよく使用されます。

  2. 型アサーション (Type Assertions): 型アサーションは、インターフェース型の変数が保持している基になる具象型、または別のインターフェース型を動的にチェックし、その値を取り出すための構文です。 構文は i.(T) の形式を取ります。ここで i はインターフェース型の変数、T はチェックしたい型です。

    型アサーションには2つの形式があります。

    • 単一の戻り値: value := i.(T) この形式は、iT 型を保持していない場合にパニック(ランタイムエラー)を引き起こします。
    • 2つの戻り値("comma ok" idiom): value, ok := i.(T) この形式は、iT 型を保持しているかどうかを安全にチェックします。ok はブール値で、アサーションが成功した場合は true、失敗した場合は false となります。成功した場合、value には T 型に変換された値が代入されます。失敗した場合、value には T 型のゼロ値が代入されます。この形式は、パニックを避けるために推奨されます。

    このコミットで問題となっているのは、まさにこの2つの戻り値を持つ型アサーションの形式です。

  3. new(T): new(T) は、型 T のゼロ値に初期化された新しい項目を割り当て、その項目へのポインタを返します。このコミットのテストコードでは t := new(T) が使用されています。

技術的詳細

このバグの核心は、Goコンパイラが型アサーション i.(Foo) の結果をどのように処理していたかにあります。通常、i.(Foo) は、成功時には Foo 型の値と true のブール値の2つの値を返します。しかし、バグのあるコンパイラは、i が既に Foo インターフェース型であるという特殊なケースにおいて、このアサーションの結果を単一の値として誤って解釈していたと考えられます。

Go言語のインターフェースは、内部的には「型」と「値」のペアとして表現されます。interface{} 型の変数 iFoo インターフェース型の値を保持している場合、その「型」の部分は Foo インターフェース型であり、「値」の部分は Foo インターフェースを実装する具象型の値です。

f, ok := i.(Foo) という型アサーションは、コンパイラにとって以下のようなロジックを生成する必要があります。

  1. i が保持する動的な型が Foo インターフェースを満たしているかチェックする。
  2. もし満たしていれば、i が保持する動的な値を Foo インターフェース型として f に代入し、oktrue を代入する。
  3. 満たしていなければ、fFoo インターフェース型のゼロ値(通常は nil)を代入し、okfalse を代入する。

バグのあるコンパイラは、i が既に Foo インターフェース型であるという状況で、ステップ1のチェックが不要であるか、あるいは異なるパスを通るべきだと誤解釈し、結果として単一の値を生成するコードを吐き出してしまった可能性があります。これにより、f, ok という2つの変数への代入と、i.(Foo) からの単一の値の出力との間で「assignment count mismatch: 2 = 1」というエラーが発生していました。

この修正は、コンパイラの型アサーション処理ロジック、特にインターフェースからインターフェースへのアサーションのケースにおいて、生成されるコードが常に2つの値を返すように調整されたことを示唆しています。これにより、Go言語の型システムの一貫性と堅牢性が向上しました。

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

このコミットでは、Go言語のコンパイラ自体のコードは直接変更されていません。代わりに、このバグを再現し、修正が適用されたことを検証するためのテストケースが追加されています。

  1. test/bugs/bug135.go の追加: このファイルは、問題の型アサーション f, ok := i.(Foo) を含む新しいテストケースです。

    package main
    
    type Foo interface { }
    
    type T struct {}
    func (t *T) foo() {}
    
    func main() {
      t := new(T);
      var i interface {};
      f, ok := i.(Foo); // この行が問題の箇所
    }
    

    このテストファイルの冒頭には // $G $D/$F.go || echo BUG: should compile というコメントがあります。これは、Goコンパイラ ($G) でこのファイルをコンパイル ($D/$F.go) した際に、エラーが発生するはずだが、最終的にはコンパイルが成功するべきである(BUG: should compile)という意図を示しています。つまり、このテストは、バグが修正される前はコンパイルエラーになるが、修正後は成功することを確認するためのものです。

  2. test/golden.out の変更: test/golden.out は、Go言語のテストスイートにおける期待される出力(ゴールデンファイル)を記録するファイルです。このファイルに、bug135.go のテスト結果が追加されています。

    =========== bugs/bug135.go
    bugs/bug135.go:13: assignment count mismatch: 2 = 1
    BUG: should compile
    

    このエントリは、バグ修正前のコンパイラが bug135.go の13行目で「assignment count mismatch: 2 = 1」というエラーを報告していたことを示しています。そして、そのエラーが「BUG: should compile」である、つまり、本来はコンパイルが成功すべきであったことを明記しています。修正後は、このエラーメッセージは表示されなくなり、テストは成功するようになります。

コアとなるコードの解説

追加された bug135.go のコードは非常にシンプルですが、Go言語の型システムにおける重要なエッジケースを突いています。

package main

type Foo interface { } // 空のインターフェースFooを定義

type T struct {}
func (t *T) foo() {} // T型にメソッドfoo()を定義(このメソッドはFooインターフェースとは無関係)

func main() {
  t := new(T); // T型のポインタを作成
  var i interface {}; // 空のインターフェース型の変数iを宣言
  // ここでiはnilであり、何の具象値も保持していない

  f, ok := i.(Foo); // iがFooインターフェースを実装しているかチェック
}

このコードのポイントは、var i interface {}; の宣言直後、inil 値と nil 型を保持しているという点です。Goのインターフェースは、具象値と具象型が両方とも nil でない限り nil ではありません。この場合、inil です。

f, ok := i.(Foo) という型アサーションは、iFoo インターフェースを実装しているかどうかをチェックします。i は現在 nil なので、このアサーションは失敗し、f には Foo インターフェースのゼロ値(nil)、ok には false が代入されることが期待されます。

しかし、バグのあるコンパイラは、この i.(Foo) の部分を処理する際に、iinterface{} 型であり、Foo もインターフェース型であるという事実から、何らかの内部的な誤解釈を生じさせていました。特に、Foo が空のインターフェースであるため、すべての型が Foo を満たすという特性が、コンパイラのロジックを複雑にしていた可能性があります。

このバグは、コンパイラがインターフェースからインターフェースへの型アサーション、特にソースインターフェースが空のインターフェースである場合や、ターゲットインターフェースも空のインターフェースである場合に、生成すべきコードの数を誤っていたことを示しています。修正により、コンパイラはこのようなケースでも常に2つの戻り値(値とokブール値)を正しく生成するようになりました。

関連リンク

  • Go言語のインターフェースに関する公式ドキュメント: https://go.dev/tour/methods/10
  • Go言語の型アサーションに関する公式ドキュメント: https://go.dev/tour/methods/15
  • Go言語の初期のバグトラッカーやメーリングリストのアーカイブ(もし公開されていれば、この特定のバグに関する議論が見つかる可能性がありますが、2009年のものは見つけにくいかもしれません。)

参考にした情報源リンク

  • Go言語の公式ドキュメント (Go Tour)
  • Go言語のソースコードリポジトリ (GitHub)
  • Go言語の型システムに関する一般的な知識
  • Go言語のコンパイラの内部構造に関する一般的な知識 (Goのコンパイラは、型チェックとコード生成の段階で、このような型アサーションをどのように処理するかを決定します。)
  • Go言語の初期のコミット履歴とテストスイートの構造。
  • Go言語の interface{} と型アサーションに関する一般的な解説記事。
  • Go言語の newmake の違いに関する解説。
  • Go言語の nil インターフェースに関する解説。