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

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

このコミットは、Go言語の型スイッチ(type switch)がインターフェースを正しく処理するかどうかを検証するための新しいテストケースを追加するものです。具体的には、空のインターフェース型として渡された値が、別の特定のインターフェース型を実装している場合に、型スイッチがその実装を正しく認識するかどうかをテストしています。このテストは、当時のGoコンパイラ/ランタイムにおけるインターフェースと型スイッチの相互作用に関する潜在的なバグを浮き彫りにすることを目的としています。

コミット

commit 90943c8ee4cf2ccef11b72ba01ea729edd8b3c66
Author: Ian Lance Taylor <iant@golang.org>
Date:   Fri Mar 20 16:30:54 2009 -0700

    Test that interfaces work in type switches.
    
    R=ken,rsc
    DELTA=30  (30 added, 0 deleted, 0 changed)
    OCL=26599
    CL=26604
---
 test/bugs/bug141.go | 30 ++++++++++++++++++++++++++++++
 test/golden.out     |  4 ++++\
 2 files changed, 34 insertions(+)

diff --git a/test/bugs/bug141.go b/test/bugs/bug141.go
new file mode 100644
index 0000000000..a2fd992c03
--- /dev/null
+++ b/test/bugs/bug141.go
@@ -0,0 +1,30 @@
+// $G $D/$F.go && $L $F.$A && ./$A.out || echo BUG: should run
+//
+// 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 S struct { i int }
+func (p *S) Get() int { return p.i }
+
+type Empty interface {
+}
+
+type Getter interface {
+	Get() int;
+}
+
+func f1(p Empty) {
+	switch x := p.(type) {
+	default: println("failed to match interface"); sys.Exit(1);
+	case Getter: break;
+	}
+
+}
+
+func main() {
+	var s S;
+	f1(&s);
+}
diff --git a/test/golden.out b/test/golden.out
index c41d2d9e17..35a6aec8c6 100644
--- a/test/golden.out
+++ b/test/golden.out
@@ -141,6 +141,10 @@ bugs/bug140.go:6: syntax error near L1
 bugs/bug140.go:7: syntax error near L2
 BUG should compile
 
+=========== bugs/bug141.go
+failed to match interface
+BUG: should run
+
 =========== fixedbugs/bug016.go
 fixedbugs/bug016.go:7: constant -3 overflows uint
 

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

https://github.com/golang/go/commit/90943c8ee4cf2ccef11b72ba01ea729edd8b3c66

元コミット内容

    Test that interfaces work in type switches.
    
    R=ken,rsc
    DELTA=30  (30 added, 0 deleted, 0 changed)
    OCL=26599
    CL=26604

変更の背景

このコミットは、Go言語の初期段階(2009年3月)において、インターフェースと型スイッチの挙動に関する潜在的なバグを特定し、再現するためのテストケースを追加するものです。コミットメッセージ「Test that interfaces work in type switches.」が示す通り、型スイッチがインターフェース型を正しく認識し、それに対応する具体的な型や、そのインターフェースを実装する別のインターフェース型に適切にディスパッチできるかどうかの検証が目的です。

test/golden.outの変更内容から、このテストは実行時に「failed to match interface」というメッセージを出力し、プログラムが異常終了することを期待しています。これは、テストが追加された時点では、Goコンパイラ/ランタイムが期待通りにインターフェースの型アサーション(型スイッチ内部で行われる)を処理できていなかったことを示唆しています。つまり、このコミットはバグを修正するものではなく、バグの存在を明確にするための再現テストを追加するものです。このようなテストは、開発プロセスにおいて、問題の特定、修正の検証、そして将来的な回帰の防止に不可欠です。

前提知識の解説

Go言語のインターフェース

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

  • メソッドセット: 型がインターフェースを実装しているかどうかは、その型の「メソッドセット」によって決まります。
    • 値型 T のメソッドセットは、レシーバが T であるすべてのメソッドを含みます。
    • ポインタ型 *T のメソッドセットは、レシーバが T または *T であるすべてのメソッドを含みます。
    • 重要な点として、ポインタ型 *T は、値型 T のメソッドセットに加えて、ポインタレシーバを持つメソッドも含むため、より多くのインターフェースを実装できる可能性があります。

Go言語の型スイッチ (Type Switch)

型スイッチは、インターフェース型の変数が保持している「動的な型」(実行時の実際の型)に基づいて異なるコードブロックを実行するための制御構造です。

構文は以下の通りです。

switch v := i.(type) {
case TypeA:
    // i の動的な型が TypeA の場合
case TypeB:
    // i の動的な型が TypeB の場合
default:
    // どのケースにもマッチしない場合
}
  • i.(type)という構文は、型スイッチの内部でのみ使用できます。
  • vは、対応するケースブロック内で、そのケースの型に型アサートされた値として利用できます。
  • 型スイッチは、インターフェースが保持する具体的な型だけでなく、そのインターフェースが別のインターフェースを実装しているかどうかもチェックできます。例えば、interface{}(空のインターフェース)型の変数が、io.Readerインターフェースを実装しているかどうかを型スイッチで確認できます。

空のインターフェース (interface{})

interface{}は、メソッドを一つも持たないインターフェースです。Go言語のすべての型は、少なくともメソッドを持たないため、nil以外のすべての値はinterface{}型として扱うことができます。これは、任意の型の値を保持できる汎用的なコンテナとしてよく使用されます。

インターフェースの内部表現(初期のGo言語における考慮点)

Go言語のインターフェース値は、内部的には2つの要素で構成されます。

  1. 型 (Type): インターフェース値が保持している実際の値の動的な型。
  2. 値 (Value): インターフェース値が保持している実際の値。

型スイッチは、この「型」の要素を調べて、どのケースにマッチするかを決定します。このコミットの背景にある問題は、この「型」の要素が、インターフェースが実装する別のインターフェース型を正しく解決できていなかった可能性を示唆しています。特に、ポインタ型がインターフェースを実装する場合の挙動は、初期のコンパイラにとって複雑な課題であった可能性があります。

技術的詳細

このコミットがテストしているシナリオは、Go言語のインターフェースの「メソッドセット」と「型スイッチ」の挙動の組み合わせに関するものです。

  1. S構造体とGetメソッド:

    type S struct { i int }
    func (p *S) Get() int { return p.i }
    

    ここで注目すべきは、Getメソッドがポインタレシーバ *S を持っている点です。これにより、*S型はGetterインターフェースを実装しますが、S型(値型)はGetterインターフェースを実装しません。なぜなら、SのメソッドセットにはGetメソッドが含まれないからです(Get*Sのメソッドセットに含まれる)。

  2. EmptyインターフェースとGetterインターフェース:

    type Empty interface {}
    type Getter interface {
    	Get() int;
    }
    

    Emptyは任意の値を保持できる空のインターフェースです。GetterGet()メソッドを持つインターフェースです。

  3. f1関数と型スイッチ:

    func f1(p Empty) {
    	switch x := p.(type) {
    	default: println("failed to match interface"); sys.Exit(1);
    	case Getter: break;
    	}
    }
    

    f1関数はEmptyインターフェースを受け取ります。この関数内で、pGetterインターフェースを実装しているかどうかを型スイッチでチェックしています。

  4. main関数での呼び出し:

    func main() {
    	var s S;
    	f1(&s);
    }
    

    main関数では、S型の変数sを宣言し、そのポインタ&sf1関数に渡しています。

問題の核心: &s*S型です。*S型はGet()メソッドを実装しているため、Getterインターフェースを実装しています。したがって、f1(&s)が呼び出された際、pEmptyインターフェース)が保持する動的な型は*Sであり、この*SGetterインターフェースを実装しているため、型スイッチのcase Getter:ブロックにマッチするはずです。

しかし、test/golden.outの期待される出力は「failed to match interface」であり、これはcase Getter:にマッチしなかったことを意味します。これは、当時のGoコンパイラ/ランタイムが、Emptyインターフェースとして渡された*S型の値がGetterインターフェースを実装していることを、型スイッチのコンテキストで正しく認識できていなかったバグが存在したことを示しています。

このバグは、インターフェースの動的な型情報と、その型が実装するインターフェースの集合を効率的かつ正確にルックアップするメカニズムに起因していたと考えられます。特に、ポインタレシーバを持つメソッドによってインターフェースが実装されるケースは、値レシーバの場合とは異なる内部処理が必要となるため、初期の実装で考慮漏れがあった可能性があります。

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

このコミットでは、以下の2つのファイルが変更されています。

  1. test/bugs/bug141.go:

    • 新規追加されたテストファイルです。
    • S構造体、Emptyインターフェース、Getterインターフェースを定義しています。
    • f1関数内で、Emptyインターフェースを受け取り、その動的な型がGetterインターフェースであるかを型スイッチで判定するロジックが含まれています。
    • main関数でSのポインタをf1に渡し、テストシナリオを実行します。
  2. test/golden.out:

    • 既存のテスト結果ファイルに、bug141.goのテスト結果の期待値が追記されています。
    • 追記された内容は、bug141.goが「failed to match interface」というメッセージを出力し、異常終了することを示しています。これは、このテストがバグを再現するためのものであることを明確にしています。

コアとなるコードの解説

test/bugs/bug141.go

// $G $D/$F.go && $L $F.$A && ./$A.out || echo BUG: should run
// この行は、テストの実行方法と期待される結果を示しています。
// $G $D/$F.go: Goコンパイラでソースファイルをコンパイルします。
// $L $F.$A: リンクします。
// ./$A.out: 実行します。
// || echo BUG: should run: もし実行が成功しなかった場合(エラー終了した場合)、"BUG: should run"と出力します。
// これは、このテストが失敗することを期待していることを示唆しています。

package main

type S struct { i int }
// Sはシンプルな構造体で、int型のフィールドiを持ちます。

func (p *S) Get() int { return p.i }
// *S型にGetメソッドを定義しています。レシーバがポインタ型であることに注意してください。
// これにより、*S型はGetterインターフェースを実装します。

type Empty interface {
}
// Emptyはメソッドを持たない空のインターフェースです。
// 任意のGoの値をEmptyインターフェース型として扱うことができます。

type Getter interface {
	Get() int;
}
// GetterはGet() intメソッドを持つインターフェースです。

func f1(p Empty) {
	switch x := p.(type) { // pの動的な型に基づいて型スイッチを行います。
	default: // どのケースにもマッチしなかった場合
		println("failed to match interface"); // エラーメッセージを出力
		sys.Exit(1); // プログラムを異常終了させます。
	case Getter: // pの動的な型がGetterインターフェースを実装している場合
		break; // 何もせず、スイッチを抜けます。
	}
}

func main() {
	var s S; // S型の変数sを宣言します。
	f1(&s);  // sのアドレス(*S型)をf1関数に渡します。
	         // *S型はGet()メソッドを持つため、Getterインターフェースを実装しています。
	         // したがって、f1内の型スイッチはcase Getterにマッチするはずです。
	         // しかし、test/golden.outの期待値から、実際にはdefaultにフォールバックすることが示唆されています。
}

test/golden.out

+=========== bugs/bug141.go
+failed to match interface
+BUG: should run

この部分は、bug141.goのテストが実行された際に、標準出力に「failed to match interface」という文字列が出力され、その後「BUG: should run」というメッセージが続くことを期待していることを示しています。これは、f1関数内の型スイッチがcase Getterにマッチせず、defaultブロックが実行されることを意味します。つまり、このテストは、*S型がEmptyインターフェースとして渡されたときに、型スイッチがGetterインターフェースの実装を正しく認識できないというバグを再現しているのです。

関連リンク

参考にした情報源リンク

  • Go言語のソースコードリポジトリ: https://github.com/golang/go
  • Go言語の初期のコミット履歴(GitHub)
  • Go言語のインターフェースとメソッドセットに関する一般的な解説記事
  • Go言語の型スイッチに関する一般的な解説記事
  • Go言語のinterface{}に関する一般的な解説記事
  • Go言語のテストフレームワークとgolden.outファイルの役割に関する情報(Goのテストシステムは時間とともに進化しているため、初期のgolden.outの利用方法は現在のテストとは異なる場合がありますが、基本的な目的は同じです)