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

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

このコミットは、Goコンパイラ(cmd/gc)における型アサーションのエラーメッセージを改善することを目的としています。具体的には、x.(T) のような型アサーションが失敗した場合に、もし *T であれば成功する可能性があった場合に、エラーメッセージ内で *T を提案するように変更されました。これは、typecheck.csubr.c 間での変換エラーのフォーマットを同期させることによって実現されています。

コミット

commit 1c2021ca142677fbfbbea950dd5a35986d86e678
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date:   Sat Sep 1 13:52:55 2012 -0400

    cmd/gc: Suggest *T in error for x.(T) if it would work.
    
    Accomplished by synchronizing the formatting of conversion errors between typecheck.c and subr.c
    
    Fixes #3984.
    
    R=golang-dev, remyoudompheng, rsc
    CC=golang-dev
    https://golang.org/cl/6500064

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

https://github.com/golang/go/commit/1c2021ca142677fbfbbea950dd5a35986d86e678

元コミット内容

cmd/gc: Suggest *T in error for x.(T) if it would work.

Accomplished by synchronizing the formatting of conversion errors between typecheck.c and subr.c

Fixes #3984.

R=golang-dev, remyoudompheng, rsc
CC=golang-dev
https://golang.org/cl/6500064

変更の背景

この変更は、GoのIssue #3984「x.(T) fails, but x.(*T) would work, suggest *T」を修正するために行われました。

Go言語において、インターフェース型から具象型への型アサーション(x.(T))は、実行時に値の実際の型が T であるかどうかをチェックし、そうであればその型に変換します。しかし、インターフェースのメソッドセットのルール(特にポインタレシーバと値レシーバの違い)により、ユーザーが意図しないエラーに遭遇することがありました。

例えば、ある型 S がインターフェース I を実装しているが、その実装がポインタレシーバ (func (s *S) M()) で行われている場合、S 型の値は I を実装しませんが、*S 型の値は I を実装します。このような状況で、ユーザーが x.(S) と誤って記述した場合、コンパイラは「SI を実装していない」というエラーを出すだけで、なぜ実装していないのか、あるいは *S であれば実装できるというヒントを提供していませんでした。

このコミット以前のコンパイラのエラーメッセージは、ユーザーが問題をデバッグする上で十分な情報を提供していませんでした。特に、ポインタレシーバのメソッドを持つ型に対する型アサーションの失敗は、Go言語のインターフェースの仕組みを完全に理解していない開発者にとっては混乱を招くものでした。この改善により、コンパイラはより親切なエラーメッセージを提供し、開発者が問題を迅速に特定し修正できるようになります。

前提知識の解説

Goの型アサーション (x.(T))

Go言語における型アサーションは、インターフェース型の変数が保持している実際の値の型を、特定の具象型に変換するために使用されます。構文は value.(Type) の形式を取ります。

  • 成功した場合: valueType 型の値を保持している場合、その値と true が返されます(v, ok := value.(Type) の形式の場合)。
  • 失敗した場合: valueType 型の値を保持していない場合、パニックが発生するか(v := value.(Type) の形式の場合)、または okfalse になります(v, ok := value.(Type) の形式の場合)。

型アサーションは、インターフェースが動的な型情報を持つGoの特性を活かす上で非常に重要な機能です。

Goのインターフェースとメソッドセット(値レシーバとポインタレシーバの違い)

Goのインターフェースは、メソッドのシグネチャの集合を定義します。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを「実装している」とみなされます。

重要なのは、メソッドが値レシーバ (func (s S) M()) で定義されているか、ポインタレシーバ (func (s *S) M()) で定義されているかによって、その型がインターフェースを実装するかどうかが変わる点です。

  • 値レシーバ (func (s S) M()):

    • S の値も、型 *S の値も、インターフェース I を実装します。
    • これは、*S の値から S の値への間接参照解除が自動的に行われるためです。
  • ポインタレシーバ (func (s *S) M()):

    • *S の値のみがインターフェース I を実装します。
    • S の値は、インターフェース I を実装しません。
    • これは、S の値から *S の値へのアドレス取得が自動的に行われないためです。インターフェースは値のコピーを保持するため、メソッドがポインタを必要とする場合、そのコピーのアドレスは取得できません。

この違いが、型アサーションのエラーメッセージの改善が必要とされた主な理由です。

Goコンパイラ (cmd/gc) の役割と typecheck.c の位置づけ

cmd/gc は、Go言語の公式コンパイラの一つであり、Goソースコードを機械語に変換する主要なツールです。コンパイルプロセスは複数のフェーズに分かれており、その中に「型チェック(Type Checking)」のフェーズがあります。

  • typecheck.c: このファイルは、Goコンパイラの型チェックフェーズの主要な部分を実装しています。ソースコード内の式や宣言がGo言語の型システム規則に準拠しているかを検証し、型の一貫性を保証します。型アサーションのチェックもこのファイルで行われ、型が一致しない場合やインターフェースの実装が不適切な場合にエラーを報告する役割を担っています。

  • subr.c: このファイルは、Goコンパイラ内で使用される様々なユーティリティ関数やサブルーチンを含んでいます。エラーメッセージのフォーマットや、型に関する補助的な処理などが含まれることがあります。このコミットでは、typecheck.csubr.c 間でエラーメッセージのフォーマットを同期させることで、一貫性のあるエラー報告を実現しています。

技術的詳細

このコミットの技術的詳細は、主に src/cmd/gc/typecheck.c 内の implements 関数の呼び出し後のエラー報告ロジックの変更にあります。

implements(n->type, t, &missing, &have, &ptr) 関数は、n->type がインターフェース t を実装しているかどうかをチェックします。

  • missing: 実装されていないメソッドのシンボルを指します。
  • have: missing と同じ名前だがシグネチャが異なるメソッドのシンボルを指します(もしあれば)。
  • ptr: n->type がインターフェース t を実装しないが、*n->type であれば実装する(つまり、ポインタレシーバが必要なメソッドがある)場合に true になります。

変更前のコードでは、implementsfalse を返した場合、主に以下の2つのエラーメッセージを出力していました。

  1. have が存在する場合(メソッド名が一致するがシグネチャが異なる場合): impossible type assertion: %lN cannot have dynamic type %T (wrong type for %S method)
  2. have が存在しない場合(メソッドが完全に欠落している場合): impossible type assertion: %lN cannot have dynamic type %T (missing %S method)

この変更では、これらのエラーメッセージのフォーマットをより詳細かつ分かりやすくするために、複数の if-else if ブロックが追加されました。特に注目すべきは、ptr フラグの利用です。

新しいエラー報告ロジックは以下のようになります。

  1. if(have && have->sym == missing->sym):

    • これは、メソッド名が一致するが、そのメソッドの型(シグネチャ)がインターフェースの要求と異なる場合を扱います。
    • エラーメッセージは「%T does not implement %T (wrong type for %S method)」となり、havewant の具体的な型情報がより詳細に表示されます。
  2. else if(ptr):

    • これがこのコミットの主要な改善点です。ptrtrue の場合、つまり現在の型 n->type ではインターフェースを実装しないが、そのポインタ型 *n->type であれば実装できる場合にこのブロックに入ります。
    • エラーメッセージは「%T does not implement %T (%S method requires pointer receiver)」となり、ポインタレシーバが必要であるという具体的なヒントが提供されます。
  3. else if(have):

    • これは、have が存在するが、have->sym == missing->sym の条件を満たさない場合(例えば、メソッド名が異なるが、implements 関数が何らかの関連性を見つけた場合など、より一般的な「間違った型」のケース)を扱います。
    • エラーメッセージは「%T does not implement %T (missing %S method)」となり、havewant の具体的な型情報が詳細に表示されます。
  4. else:

    • 上記のどの条件にも当てはまらない場合、つまりメソッドが完全に欠落している最も一般的なケースを扱います。
    • エラーメッセージは「%T does not implement %T (missing %S method)」となります。

これらの変更により、コンパイラは型アサーションの失敗原因をより正確に診断し、特にポインタレシーバの要件に関する一般的な誤解を解消するための具体的なガイダンスを提供できるようになりました。subr.c との同期は、これらの新しいエラーメッセージのフォーマットがコンパイラ全体で一貫していることを保証するためのものです。

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

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -735,14 +735,20 @@ reswitch:
 		}
 		if(n->type != T && n->type->etype != TINTER)
 		if(!implements(n->type, t, &missing, &have, &ptr)) {
-\t\t\tif(have)
-\t\t\t\tyyerror("impossible type assertion: %lN cannot have dynamic type %T"
-\t\t\t\t\t" (wrong type for %S method)\n\thave %S%hT\n\twant %S%hT",
-\t\t\t\t\tl, n->type, missing->sym, have->sym, have->type,
-\t\t\t\t\tmissing->sym, missing->type);
+\t\t\tif(have && have->sym == missing->sym)
+\t\t\t\tyyerror("impossible type assertion:\n\t%T does not implement %T (wrong type for %S method)\n"
+\t\t\t\t\t"\t\thave %S%hhT\n\t\twant %S%hhT", n->type, t, missing->sym,
+\t\t\t\t\thave->sym, have->type, missing->sym, missing->type);
+\t\t\telse if(ptr)
+\t\t\t\tyyerror("impossible type assertion:\n\t%T does not implement %T (%S method requires pointer receiver)",
+\t\t\t\t\tn->type, t, missing->sym);
+\t\t\telse if(have)
+\t\t\t\tyyerror("impossible type assertion:\n\t%T does not implement %T (missing %S method)\n"
+\t\t\t\t\t"\t\thave %S%hhT\n\t\twant %S%hhT", n->type, t, missing->sym,
+\t\t\t\t\thave->sym, have->type, missing->sym, missing->type);
 \t\t\telse
-\t\t\t\tyyerror("impossible type assertion: %lN cannot have dynamic type %T"
-\t\t\t\t\t" (missing %S method)", l, n->type, missing->sym);
+\t\t\t\tyyerror("impossible type assertion:\n\t%T does not implement %T (missing %S method)",
+\t\t\t\t\tn->type, t, missing->sym);
 \t\t\tgoto error;
 \t\t}
 \t\tgoto ret;

test/interface/explicit.go

--- a/test/interface/explicit.go
+++ b/test/interface/explicit.go
@@ -15,6 +15,10 @@ type T struct {
 
 var t *T
 
+type X int
+
+func (x *X) M() {}
+
 type I interface {
 	M()
 }
@@ -66,6 +70,8 @@ func (Int) M(float64) {}
 
 var _ = m.(Int) // ERROR "impossible type assertion"
 
+var _ = m.(X) // ERROR "pointer receiver"
+
 var ii int
 var jj Int
 

コアとなるコードの解説

src/cmd/gc/typecheck.c の変更点

この変更の核心は、implements 関数がインターフェースの実装失敗を報告した際に、より具体的で役立つエラーメッセージを生成するための条件分岐の追加です。

  • 変更前:

    • if(have): メソッド名が一致するがシグネチャが異なる場合に、wrong type for %S method というメッセージを出力していました。
    • else: メソッドが完全に欠落している場合に、missing %S method というメッセージを出力していました。
    • これらのメッセージは、ポインタレシーバの必要性など、具体的な原因を特定するのに十分ではありませんでした。
  • 変更後:

    • if(have && have->sym == missing->sym): これは、インターフェースが要求するメソッドと、具象型が持つ同名のメソッドのシグネチャが異なる場合にトリガーされます。エラーメッセージは、wrong type for %S method となり、havewant の具体的な型情報(%hhT フォーマット指定子を使用)が詳細に表示されるようになりました。これにより、ユーザーはシグネチャの不一致を正確に把握できます。
    • else if(ptr): この新しい条件は、implements 関数が ptr フラグを true に設定した場合にトリガーされます。これは、現在の型 n->type ではインターフェースを実装しないが、そのポインタ型 *n->type であれば実装できることを意味します。この場合、コンパイラは「%S method requires pointer receiver」という非常に具体的なヒントを提供します。これは、Goのインターフェースとレシーバの仕組みに関する一般的な誤解を解消する上で非常に役立ちます。
    • else if(have): この条件は、have が存在するが、have->sym == missing->sym の条件を満たさない場合(例えば、メソッド名が異なるが、implements 関数が何らかの関連性を見つけた場合など)を扱います。この場合も、missing %S method というメッセージと共に、havewant の具体的な型情報が詳細に表示されます。
    • else: 上記のどの条件にも当てはまらない場合、つまりメソッドが完全に欠落している最も一般的なケースを扱います。この場合も、missing %S method というメッセージが出力されます。

これらの変更により、コンパイラは型アサーションの失敗原因をより正確に診断し、特にポインタレシーバの要件に関する一般的な誤解を解消するための具体的なガイダンスを提供できるようになりました。エラーメッセージのフォーマットも改善され、より読みやすくなっています(改行の追加など)。

test/interface/explicit.go の変更点

このテストファイルには、新しい型 X と、そのポインタレシーバメソッド M() が追加されました。

type X int

func (x *X) M() {}

そして、既存のインターフェース I (type I interface { M() }) に対して、m.(X) という型アサーションが追加されました。ここで mI 型の変数です。

var _ = m.(X) // ERROR "pointer receiver"

このテストケースは、m*X 型の値を保持している場合に m.(X) が失敗し、新しいエラーメッセージ「pointer receiver」が出力されることを検証します。これは、X 型自体は I を実装しないが、*X 型は I を実装するというGoのインターフェースのルールを反映しており、このコミットによって改善されたエラーメッセージが正しく機能することを確認しています。

関連リンク

参考にした情報源リンク