x.(T):インターフェースから値を取り出す
インターフェースの値は、保持している具体的な型を隠しています。型アサーション x.(T) はそれを取り戻すよう求めます。
出力:
gopher 6
0 false
string of length 6
x はインターフェース型でなければなりません。具体的な型に対してアサーションするとコンパイルに失敗します:invalid operation: s (variable of type string) is not an interface。
1値の形はpanicする
アサーションが間違っていると、1値の形は両方の型を示すメッセージとともにpanicします。
出力:
recovered: interface conversion: interface {} is int, not string
1値の形を使うのは、別の型が来たらクラッシュさせたいプログラミングのエラーであるときだけにします。それ以外はすべてカンマokを使います。失敗したとき、カンマokは T のゼロ値と false を返します。
nilのインターフェースに対するアサーションも失敗します。var v any; v.(string) は interface conversion: interface {} is nil, not string でpanicし、カンマokの形は "", false を返します。
別のインターフェースへのアサーション
T はインターフェース型でも構いません。その場合、動的な値が T を実装していればアサーションは成功し、結果は同じ動的な値を保持します。標準ライブラリはこの方法で、省略可能な機能があるかをチェックしています。
io.Copy は、コピー元が io.WriterTo を実装しているかをチェックし、実装していればそれを使います。fmt は Stringer と error をチェックします。このパターンによって、小さなインターフェースを小さいまま保ちつつ、呼び出し側はより豊かな実装の恩恵を受けられます。
型スイッチ
値がいくつかの型のどれかでありうるとき、型スイッチはアサーションの連鎖の代わりになります。x.(type) は switch の中でしか使えません。
出力:
hi
integer 7
integer 8
3.14
nil
error: boom
unhandled []int
知っておきたいルールは次のとおりです。
- 型が1つのcaseでは、
xはその型になります。複数の型を並べたcaseとdefaultでは、xは元のインターフェースの型(ここではany)です。 - caseは順にチェックされます。値は複数のインターフェースを満たしうるので、より具体的なインターフェースを広いものより前に置きます。
case nilに一致するのはnilのインターフェースだけで、nilポインタを保持するインターフェースは一致しません。- 型スイッチに
fallthroughはありません。
errors.As:ラップされたエラーのためのアサーション
エラーは fmt.Errorf("load config: %w", err) のように文脈付きでラップされることがよくあります。直接の型アサーションは最も外側のエラーしか見ないので、中のエラーを見逃します。errors.As は連鎖をたどります。
出力:
type assertion finds it: false
errors.As finds it: open /no/such/file
エラーに型アサーションを使うのは、エラーがラップされていないとわかっているときだけで、実際にはほぼ皆無です。errors.As、errors.Is、独自のエラー型の定義はカスタムエラーのページで扱っています。
アサーション、変換、ジェネリクスの使い分け
| 手元にあるもの | ほしいもの | 使うもの |
|---|---|---|
int | float64 | 変換:float64(n) |
int を保持する any | その int | アサーション:v.(int) |
いくつかの型のどれかである any | 型で分岐する | 型スイッチ |
ラップされているかもしれない error | 特定のエラー型 | errors.As |
| 多くの型で動く関数 | コンパイル時の安全性 | ジェネリクス |
any と型スイッチだらけのコードは、ジェネリクスや適切なインターフェースのほうが意図をよく表し、間違いをコンパイル時に捕まえられるというサインであることがよくあります。
よくある間違い
- 信頼できないデータにpanicする形を使う。 デコードしたJSON、
any型のマップの値、プラグインからの入力には、常にカンマokを使います。 - 間違った数値型にアサーションする。 JSONの数値は
anyにfloat64としてデコードされるので、v.(int)は失敗します。 - ポインタが格納されているのに値の型にアサーションする。 インターフェースが
*Userを保持していれば、v.(User)は失敗します。v.(*User)でアサーションします。 - エラーに型アサーションを使う。
errors.Asを使います。
よくある質問
Goの型アサーションとは何ですか?
x がインターフェースの値であるときの式 x.(T) です。T が具体的な型なら、x に格納された値を T として返します。T がインターフェース型なら、格納された値が T も実装しているかをチェックします。1値の形はチェックに失敗するとpanicし、2値の形 v, ok := x.(T) は結果を ok で報告します。
Goでインターフェースの値の型を確認するには?
型スイッチを使います:switch v := x.(type) { case int: ...; case string: ...; default: ... }。各caseの中で v はそのcaseの型になります。1つの型だけなら、カンマokのアサーション s, ok := x.(string) のほうが短く書けます。デバッグでは fmt.Printf("%T", x) で型名を表示できます。
Goの型アサーションと型変換の違いは何ですか?
変換 T(x) は、float64(n) のように、ある型の値を別の型に変えるものです。許されるかどうかはコンパイラが判断し、実行時に失敗することはありません。アサーション x.(T) はインターフェースの値にだけ使え、中にどの型が格納されているかを実行時にチェックします。intを保持する any に対して int(x) とは書けず、x.(int) が必要です。
エラーの型を調べるのに型アサーションを使うべきですか?
いいえ。errors.As(err, &target) を使います。直接のアサーション err.(*MyError) は外側のエラーしか見ないので、エラーが fmt.Errorf("...: %w", err) でラップされた途端に失敗します。errors.As はラップの連鎖をたどります。