インターフェースはメソッドの集合
インターフェース型はメソッドのシグネチャを列挙します。それらのメソッドを持つ型はどれでも、そう宣言することなくインターフェースを満たします。
Rect も Circle も Shape に触れていません。これが暗黙の実装で、Goのインターフェースを特徴づける性質です。他のパッケージの型がすでに満たしているインターフェースを、そのコードに触れることなく自分のパッケージで定義できます。
標準ライブラリの小さなインターフェース
Goのコードは、メソッドが1つか2つのインターフェースを好みます。最も重要なものは次のとおりです。
| インターフェース | メソッド | 使っているもの |
|---|---|---|
fmt.Stringer | String() string | fmt の表示 |
error | Error() string | 失敗しうるすべての関数 |
io.Reader | Read(p []byte) (n int, err error) | ファイル、ネットワーク、gzip、HTTPのボディ |
io.Writer | Write(p []byte) (n int, err error) | ファイル、バッファ、ハッシュ、HTTPのレスポンス |
sort.Interface | Len、Less、Swap | sort パッケージ |
http.Handler | ServeHTTP(w, r) | net/http |
io.Reader はメソッドが1つなので、数十の型がこれを実装しており、io.Reader を受け取る関数はそのすべてで動きます。
Goのことわざに「インターフェースが大きいほど、抽象化は弱くなる」というものがあります。大きなインターフェースは小さなものを組み合わせて作ります。io.ReadWriter は Reader と Writer を合わせたもので、インターフェースを別のインターフェースに埋め込んで書きます。
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)
実行時のコストはかかりません。*LogWriter が Write(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.Openはio.Readerではなく*os.Fileを返します。
関連する習慣として、インターフェースは実装する側ではなく使う側で定義します。サービスがユーザーを Get(id) できるものを必要とするなら、サービスのパッケージにメソッド1つのインターフェースを宣言し、データベースのパッケージは構造体を公開するだけにします。
インターフェースの値を比較する
2つのインターフェースの値は、動的な型が同一で、動的な値が等しいときに等しくなります。動的な型が比較できないもの(スライス、マップ)だと、== はコンパイルできますが実行時にpanicします:runtime error: comparing uncomparable type []int。
よくある間違い
- 型付きのnilポインタをインターフェースとして返す。 リテラルの
nilを返します。 - 早すぎるインターフェース。 まず具体的な型を書きます。2つ目の実装やテストが必要になったらインターフェースを追加します。
- インターフェースへのポインタ。
*io.Readerが正しいことはほとんどありません。ポインタを格納すれば、インターフェースはすでにポインタを保持しています。 - 大きなインターフェース。 メソッドが10個あるインターフェースは実装も偽物作りも大変です。分割しましょう。
よくある質問
Goでインターフェースを実装するには?
インターフェースが列挙しているメソッドを、同じ名前とシグネチャで自分の型に定義します。implements キーワードはありません。*File が Read(p []byte) (int, error) を持っていれば、自動的に io.Reader になります。コンパイラは、値をインターフェース型に代入する場所でこれをチェックします。
Goの空のインターフェースやanyとは何ですか?
interface{} はメソッドを持たないので、どの型もこれを満たします。Go 1.18以降、any は interface{} の組み込みの別名です。any 型の値は何でも保持できますが、具体的な型を取り出すには型アサーションか型スイッチが必要です。
nilポインタを代入したのに、なぜGoのインターフェースはnilではないのですか?
インターフェースの値は型と値を保持しているからです。nilの *MyError を error に代入すると、型が *MyError で値がnilのインターフェースになり、そのインターフェースは nil と等しくありません。エラーがないときは、型付きのnilポインタではなくリテラルの nil を返します。
型がインターフェースを実装していることをコンパイル時に確認するには?
パッケージレベルにブランクへの代入を追加します:var _ io.Reader = (*MyReader)(nil)。*MyReader にメソッドが欠けていれば、欠けているメソッド名を示すメッセージとともにビルドが失敗します。