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

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

このコミットは、Go言語の標準ライブラリである append 関数の挙動に関するテストケースを追加するものです。具体的には、異なる名前付き型(named types)でありながら、基底となる要素型(element type)が同じスライス型同士を append 関数に渡した場合のコンパイラの振る舞いを検証しています。

コミット

test: test append with two different named types with same element type

R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/5615045

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

https://github.com/golang/go/commit/3692726f32f4cff4429e893830871d9b50b9816b

元コミット内容

test: test append with two different named types with same element type

このコミットは、append 関数が、要素型が同じであるものの、異なる名前を持つスライス型を引数として受け入れるかどうかをテストします。

変更の背景

Go言語の append 関数は、第一引数にスライス、第二引数にそのスライスの要素型と同じ型の可変長引数(またはスライス)を取ります。このコミットが追加された背景には、Goの型システムにおける「名前付き型」と「基底型」の概念、そして append 関数が引数の型をどのように解釈するかという点があります。

Goでは、既存の型に新しい名前を付けて新しい型を定義することができます(type MyInt int のように)。このような名前付き型は、基底型が同じであっても、異なる型として扱われます。しかし、append のような特定の組み込み関数においては、引数の型が厳密に一致しなくても、特定の条件(この場合は要素型の一致)を満たせば受け入れられる場合があります。

このコミットは、このような微妙な型互換性の挙動が期待通りに機能するかどうかを確認するためのテストの不足を補うものです。特に、異なる名前付きスライス型が、append の第二引数として渡された場合にコンパイラがエラーを出さずに処理できることを保証するために追加されました。

前提知識の解説

Go言語のスライス (Slice)

Go言語のスライスは、配列をラップした動的なデータ構造です。スライスは []Type の形式で宣言され、Type はスライスの要素の型を示します。スライスは参照型であり、基底となる配列の一部を参照します。

Go言語の名前付き型 (Named Types)

Goでは、type NewType UnderlyingType の構文を使って新しい型を定義できます。この NewType は「名前付き型」と呼ばれ、UnderlyingType はその「基底型」です。 例:

type MyInt int
type MyString string
type MySlice []int

MyIntint とは異なる型として扱われます。たとえ基底型が同じでも、異なる名前付き型の間には暗黙的な変換は行われません。明示的な型変換が必要です。

append 関数の挙動

Goの組み込み関数 append は、スライスに要素を追加するために使用されます。そのシグネチャは概念的に以下のようになります(実際にはジェネリックに動作します):

func append(slice []T, elems ...T) []T

ここで T はスライスの要素型です。重要なのは、第二引数 elems...T または []T の形式で、第一引数のスライス slice の要素型 T と互換性がある必要があるという点です。

このコミットのポイントは、append の第二引数に渡されるスライスが、第一引数のスライスと「同じ基底型を持つが、異なる名前付き型である」場合にどうなるか、という点です。Goの仕様では、append(s []T, x ...U) の形式で、UT に代入可能であるか、または U[]T に代入可能なスライス型である場合に、append が機能することが許容されています。このテストケースは、後者の「U[]T に代入可能なスライス型である」という条件を、名前付き型が絡むケースで検証しています。

技術的詳細

このコミットで追加されたテストケース verifyType() は、Goの型システムにおけるスライスの互換性、特に append 関数の引数としての振る舞いを検証しています。

  1. 名前付きスライス型の定義:

    type T1 []int
    type T2 []int
    

    ここで T1T2 は、どちらも基底型が []int である異なる名前付き型として定義されています。Goの型システムでは、これらは異なる型として扱われます。例えば、T1 型の変数を T2 型の変数に直接代入することはできません。

  2. append 関数の呼び出し:

    t1 := T1{1}
    t2 := T2{2}
    verify("T1", append(t1, t2...), T1{1, 2})
    

    ここで append(t1, t2...) が呼び出されています。

    • t1T1 型(基底型 []int)。
    • t2T2 型(基底型 []int)。t2...t2 スライスの要素を個別の引数として展開します。

    append 関数の仕様では、第二引数(可変長引数)がスライスとして渡される場合、そのスライスは第一引数のスライスの要素型と同じ要素型を持つ必要があります。このケースでは、t1 の要素型は int であり、t2 の要素型も int です。したがって、appendT1T2 が異なる名前付き型であっても、その基底となる要素型が一致するため、この操作を許可します。

  3. 戻り値の型: append 関数の戻り値の型は、常に第一引数のスライスと同じ型になります。この場合、append(t1, t2...) の結果は T1 型のスライスになります。テストでは、期待される結果 T1{1, 2} と比較することで、この挙動が正しいことを確認しています。

このテストは、Goコンパイラが append の型チェックにおいて、名前付き型の厳密な一致ではなく、基底となる要素型の互換性を考慮していることを明確に示しています。これは、Goの型システムが柔軟性と厳密性のバランスを取っている一例と言えます。

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

変更は test/append.go ファイルに対して行われました。

  1. main 関数に verifyType() の呼び出しが追加されました。

    --- a/test/append.go
    +++ b/test/append.go
    @@ -27,6 +27,7 @@ func main() {
     	}
     	verifyStruct()
     	verifyInterface()
    +\tverifyType()\n
     }
    
  2. 新しいテスト関数 verifyType() が追加されました。

    --- a/test/append.go
    +++ b/test/append.go
    @@ -230,3 +231,17 @@ func verifyInterface() {\n
     	verify("interface l", append(s), s)\n
     	verify("interface m", append(s, e...), r)\n
     }\n
    +\n
    +type T1 []int\n
    +type T2 []int\n
    +\n
    +func verifyType() {\n
    +\t// The second argument to append has type []E where E is the\n
    +\t// element type of the first argument.  Test that the compiler\n
    +\t// accepts two slice types that meet that requirement but are\n
    +\t// not assignment compatible.  The return type of append is\n
    +\t// the type of the first argument.\n
    +\tt1 := T1{1}\n
    +\tt2 := T2{2}\n
    +\tverify("T1", append(t1, t2...), T1{1, 2})\n
    +}\n
    

コアとなるコードの解説

追加された verifyType 関数は以下の要素で構成されています。

  1. 名前付きスライス型の定義:

    type T1 []int
    type T2 []int
    

    T1T2 は、どちらも []int を基底型とする新しい名前付きスライス型です。これにより、Goの型システムがこれらを異なる型として扱うことを保証します。

  2. 変数の初期化:

    t1 := T1{1}
    t2 := T2{2}
    

    t1T1 型のスライスとして [1] で初期化され、t2T2 型のスライスとして [2] で初期化されます。

  3. append 関数のテスト:

    verify("T1", append(t1, t2...), T1{1, 2})
    
    • append(t1, t2...): ここがテストの核心です。t1T1 型のスライス、t2...T2 型のスライスの要素を展開したものです。Goの append 関数は、第二引数がスライスとして渡される場合、そのスライスの要素型が第一引数のスライスの要素型と一致していれば、異なる名前付き型であっても受け入れます。この場合、T1 の要素型も intT2 の要素型も int であるため、この呼び出しは有効です。
    • verify(...): これはテストフレームワークの一部であり、append の結果が期待される値 T1{1, 2} と一致するかどうかを検証します。append の結果は第一引数 t1 の型である T1 型のスライスになります。

このテストは、Goコンパイラが append の型チェックにおいて、名前付き型の厳密な同一性ではなく、基底となる要素型の互換性を適切に判断していることを確認するための重要なケースです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (特に src/builtin/builtin.go やコンパイラの型チェック関連コード)
  • Go言語の型システムに関する一般的な情報源 (例: Effective Go, Go言語の仕様書)
  • GitHub上のGoリポジトリ: https://github.com/golang/go