Menu

Go言語のインターフェース:暗黙の実装、any、nilの罠

Goのインターフェースは暗黙に満たされます。必要なメソッドを持つ型はどれでもそれを実装します。io.Readerやfmt.Stringerのような小さなインターフェース、空のインターフェースany、nilインターフェースの落とし穴、そして「インターフェースを受け取り、構造体を返す」を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

インターフェースはメソッドの集合

インターフェース型はメソッドのシグネチャを列挙します。それらのメソッドを持つ型はどれでも、そう宣言することなくインターフェースを満たします。

RectCircleShape に触れていません。これが暗黙の実装で、Goのインターフェースを特徴づける性質です。他のパッケージの型がすでに満たしているインターフェースを、そのコードに触れることなく自分のパッケージで定義できます。

標準ライブラリの小さなインターフェース

Goのコードは、メソッドが1つか2つのインターフェースを好みます。最も重要なものは次のとおりです。

インターフェースメソッド使っているもの
fmt.StringerString() stringfmt の表示
errorError() string失敗しうるすべての関数
io.ReaderRead(p []byte) (n int, err error)ファイル、ネットワーク、gzip、HTTPのボディ
io.WriterWrite(p []byte) (n int, err error)ファイル、バッファ、ハッシュ、HTTPのレスポンス
sort.InterfaceLenLessSwapsort パッケージ
http.HandlerServeHTTP(w, r)net/http

io.Reader はメソッドが1つなので、数十の型がこれを実装しており、io.Reader を受け取る関数はそのすべてで動きます。

Goのことわざに「インターフェースが大きいほど、抽象化は弱くなる」というものがあります。大きなインターフェースは小さなものを組み合わせて作ります。io.ReadWriterReaderWriter を合わせたもので、インターフェースを別のインターフェースに埋め込んで書きます。

any:空のインターフェース

interface{} はメソッドを持たないので、どの型もこれを満たします。Go 1.18で別名として any が追加され、両者はまったく同じです。

any の値は何でも保持できますが、型アサーションや型スイッチで具体的な型を取り戻すまでは、ほとんど何もできません。型の集合がわかっているなら、本物のインターフェースかジェネリクスを使いましょう。any が適しているのは、形のわからないJSONのデコード結果のような本当に動的なデータや、表示の場面です。

インターフェースの値に入っているもの

インターフェースの値は、動的な型と動的な値のペアです。var s Shape = Rect{3, 4} は型 Rect と値のコピーを保存します。s.Area() を呼ぶと、実行時に Rect のメソッドが検索されます。

インターフェースが nil になるのは、両方が空のときだけです。このルールが、Goで最もわかりにくいバグを引き起こします。

nilインターフェースの落とし穴

インターフェースにnilポインタを格納すると、nilでないインターフェースになります。

出力:

false
*main.MyError true
true

validate(true) は、型 *MyError と値 nil を保持する error インターフェースを返します。インターフェースは型を持っているので nil と等しくなく、呼び出し側の if err != nil の分岐が実行されます。そこで err.Error() を呼ぶと、nilレシーバーのフィールドへのアクセスでpanicします。

直し方は簡単です。変数を具体的なポインタ型ではなく error として宣言するか、成功の経路ではリテラルの nil を返します。結果が error の関数から、具体的なエラーのポインタ型を返してはいけません。同じ罠はエラーに限らず、あらゆるインターフェースに当てはまります。

型がインターフェースを実装しているか確認する

実装のチェックは、値がインターフェースに代入される場所で行われます。まだどのコードもそうしていなければ、メソッドのシグネチャの間違いは気づかれません。パッケージレベルでのブランクへの代入で、チェックを明示的にできます。

var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)

実行時のコストはかかりません。*LogWriterWrite(p []byte) (int, error) ではなく Write(p []byte) error を持っていれば、ビルドが失敗します。

cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
		have Write([]byte) error
		want Write([]byte) (int, error)

ポインタレシーバーとインターフェース

メソッドがポインタレシーバーを持つと、そのメソッドを持つのはポインタ型だけです。*Counter はそれでインターフェースを満たしますが、Counter は満たしません。コンパイラは Counter does not implement Incrementer (method Inc has pointer receiver) と言います。インターフェースには &Counter{} を格納します。メソッドセットについてはメソッドのページで説明しています。

インターフェースを受け取り、構造体を返す

Goのよく知られた指針です。関数はインターフェースの引数を受け取り、具体的な型を返すべきです。

  • インターフェースを受け取る と、呼び出し側はテスト用の偽物を含め、合うものなら何でも渡せます。データを読む関数は *os.File ではなく io.Reader を受け取るべきです。
  • 具体的な型を返す と、呼び出し側はそのすべてのメソッドとフィールドを使え、nilインターフェースの罠も避けられます。os.Openio.Reader ではなく *os.File を返します。

関連する習慣として、インターフェースは実装する側ではなく使う側で定義します。サービスがユーザーを Get(id) できるものを必要とするなら、サービスのパッケージにメソッド1つのインターフェースを宣言し、データベースのパッケージは構造体を公開するだけにします。

インターフェースの値を比較する

2つのインターフェースの値は、動的な型が同一で、動的な値が等しいときに等しくなります。動的な型が比較できないもの(スライス、マップ)だと、== はコンパイルできますが実行時にpanicします:runtime error: comparing uncomparable type []int

よくある間違い

  • 型付きのnilポインタをインターフェースとして返す。 リテラルの nil を返します。
  • 早すぎるインターフェース。 まず具体的な型を書きます。2つ目の実装やテストが必要になったらインターフェースを追加します。
  • インターフェースへのポインタ。 *io.Reader が正しいことはほとんどありません。ポインタを格納すれば、インターフェースはすでにポインタを保持しています。
  • 大きなインターフェース。 メソッドが10個あるインターフェースは実装も偽物作りも大変です。分割しましょう。

よくある質問

Goでインターフェースを実装するには?

インターフェースが列挙しているメソッドを、同じ名前とシグネチャで自分の型に定義します。implements キーワードはありません。*FileRead(p []byte) (int, error) を持っていれば、自動的に io.Reader になります。コンパイラは、値をインターフェース型に代入する場所でこれをチェックします。

Goの空のインターフェースやanyとは何ですか?

interface{} はメソッドを持たないので、どの型もこれを満たします。Go 1.18以降、anyinterface{} の組み込みの別名です。any 型の値は何でも保持できますが、具体的な型を取り出すには型アサーションか型スイッチが必要です。

nilポインタを代入したのに、なぜGoのインターフェースはnilではないのですか?

インターフェースの値は型と値を保持しているからです。nilの *MyErrorerror に代入すると、型が *MyError で値がnilのインターフェースになり、そのインターフェースは nil と等しくありません。エラーがないときは、型付きのnilポインタではなくリテラルの nil を返します。

型がインターフェースを実装していることをコンパイル時に確認するには?

パッケージレベルにブランクへの代入を追加します:var _ io.Reader = (*MyReader)(nil)*MyReader にメソッドが欠けていれば、欠けているメソッド名を示すメッセージとともにビルドが失敗します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める