インターフェース型
インターフェース型は、具体型を指定せず「呼び出せるメソッドの条件」だけを書いた型です。条件を満たす値なら、中身の具体型が違っても同じ変数に入れられます。
type Puller interface {
Pull(p []byte) (n int, err error)
}メソッドを1つも宣言しないインターフェースは空インターフェースと呼ばれ、任意の型(インターフェース型を含む)を格納できます。anyは空インターフェースの別名です。anyに何でも入れられるから型安全、とはならず、型アサーションや型スイッチなしに具体型のメソッドやフィールドへアクセスできません。
インターフェース値の中身
インターフェース変数は、型の見え方が2段階あります。プログラムを書いてコンパイルするときに決まる静的型と、プログラムが動いて値を代入したときに決まる動的型・動的値です。静的型は変数宣言の型で、実行中も変わりません。
静的型(static type)
var r io.Readerのio.Readerが静的型です。コンパイラはrからReadなど、io.Readerで許されたメソッドだけ呼べると判断します。この判断はビルド時に行われ、実行中にrの中身が*os.Fileでも別の型でも、静的型はio.Readerのままです。
動的型(dynamic type)
代入のたびに決まる、実際に入っている具体型です。rに*os.Fileを入れれば動的型は*os.File、別の値を入れ直せば動的型も変わります。
動的値(dynamic value)
代入のたびに決まる、その具体型の値そのものです。*os.Fileを入れていれば、ファイルオブジェクトへのポインタ(またはnil)が動的値です。
静的型はコンパイラ向けの約束、動的型と動的値は実行時の中身です。nilの判定で混乱しやすいのは、動的型だけ付いて動的値がnilのときです。
暗黙の実装
implementsのような宣言はありません。型がインターフェースが要求するメソッドをすべて備えていれば、宣言なくそのインターフェースを実装したとみなされます。コンパイラは代入時にメソッド集合の包含関係をチェックします。値型Tとポインタ型*Tではメソッド集合が異なるため、代入する値の型にも注意が必要です。
type Puller interface {
Pull(p []byte) (n int, err error)
}
type File struct{}
func (f File) Pull(p []byte) (int, error) {
return 0, nil
}
var p Puller = File{} // FileがPullerのメソッドを持つので代入可能nilの二段階
インターフェース変数のr == nilは、動的型と動的値の両方がないときだけtrueになります。ポインタ変数のf == nilとは別の判定です。
動的型も動的値もない
ゼロ値のインターフェース変数は、中身のペアが空です。
var r io.Reader
fmt.Println(r == nil) // true動的型だけある
nilポインタをインターフェースに入れると、動的型は*os.File、動的値はnilのペアが入ります。ポインタ自体はnilでも、インターフェースとしては空ではないためr2 == nilはfalseです。
var f *os.File
fmt.Println(f == nil) // true: ポインタ変数としてはnil
var r2 io.Reader = f
fmt.Println(r2 == nil) // false: 動的型 *os.File がある中身がnilか調べる
インターフェースのnil判定だけでは、中身のポインタがnilか分かりません。型アサーションで取り出してから調べます。
if f, ok := r2.(*os.File); ok {
fmt.Println(f == nil) // true: 中身のポインタはnil
}埋め込み
インターフェース定義に別のインターフェース型名を並べると埋め込みになります。埋め込んだ側は、内側のインターフェースが要求するメソッドをすべて引き継ぎます。構造体の埋め込みのようにフィールドが昇格するわけではなく、メソッドの条件が合成されます。
type Puller interface {
Pull(p []byte) (n int, err error)
}
type Pusher interface {
Push(p []byte) (n int, err error)
}
type Duplex interface {
Puller
Pusher
}要求されるメソッド
DuplexはPullとPushの両方を要求します。Duplexのメソッド集合はPullerとPusherの和集合です。次のようにメソッドを直書きしたのと同じ意味です。
type Duplex interface {
Pull(p []byte) (n int, err error)
Push(p []byte) (n int, err error)
}具体型と代入
PullとPushの両方を備えた型はDuplexにもPullerにもPusherにも代入できます。どちらか一方だけの型はDuplexを満たしません。
type Pipe struct{}
func (Pipe) Pull(p []byte) (int, error) { return 0, nil }
func (Pipe) Push(p []byte) (int, error) { return len(p), nil }
type OneWay struct{}
func (OneWay) Pull(p []byte) (int, error) { return 0, nil }
var d Duplex = Pipe{} // OK
var pull Puller = Pipe{}
var push Pusher = Pipe{}
// var bad Duplex = OneWay{} // コンパイルエラー: Push がない命名の慣習
1メソッドのインターフェースは、メソッド名に-erを付けた名前にすることが多いです。Reader, Writer, Stringerなどが例です。
小さなインターフェース
インターフェースは必要なメソッドだけを宣言するのが慣習です。io.Readerのように1メソッドのインターフェースは、多くの型で再利用しやすくなります。
型アサーション
インターフェース値から具体型を取り出すには型アサーションを使います。s, ok := v.(T)の2値形式で取り出し、動的型がTでなければokがfalseになります。
var v any = "hello"
s, ok := v.(string)
if !ok {
// 動的型がstringでない
return
}
// ここではsをstringとして使えるokを受け取らないs := v.(T)は、動的型がTでないときにパニックします。ユーザー入力や外部データから復元するときは、必ず2値形式で分岐します。
型スイッチ
複数の動的型を分岐するときはswitch v := x.(type)の型スイッチを使います。動的型に応じてcaseが選ばれるだけで、どのcaseにも当てはまらなくてもパニックしません。
func show(x any) {
switch v := x.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
default:
fmt.Println("other type")
}
}インターフェース実装の確認
暗黙の実装では、コンパイラは代入が起きたときにメソッド集合を見てインターフェースを満たすか判断します。型定義の近くにインターフェース変数への代入を書いておくと、メソッドの追加漏れやレシーバ型の取り違えをビルド時に検知できます。
書き方
型定義の直後に、満たすべきインターフェース型への代入を書きます。
type Pipe struct{}
func (Pipe) Pull(p []byte) (int, error) { return 0, nil }
var _ Puller = Pipe{}var _ Puller = ...は通常の変数宣言と代入であり、コンパイル時に代入可能性がチェックされます。左辺の_は値を使わない識別子です。右辺にPipe{}や(*Pipe)(nil)を置けるのは、実際に使うインスタンスではなく代入可能性の確認だけが目的だからです。
値型とポインタ型
代入の判定は、右辺の型のメソッド集合で行われます。ポインタレシーバのメソッドだけを持つ型では、値型ではなくポインタ型を右辺に置く必要があります。
type Sink struct{}
func (s *Sink) Pull(p []byte) (int, error) { return 0, nil }
// var _ Puller = Sink{} // コンパイルエラー: SinkにはPullがない
var _ Puller = (*Sink)(nil) // OK: *Sinkのメソッド集合にPullがあるSinkの例では次のようになります。
| 右辺 | 型 | 値 | Pullerへの代入 |
|---|---|---|---|
Sink{} | Sink | ゼロ値の構造体 | 不可(SinkにPullがない) |
(*Sink)(nil) | *Sink | nil | 可(*SinkにPullがある) |
(*Sink)(nil)は実体を作らず、右辺の型が*Sinkであることだけをコンパイラに伝えます。代入可能性の確認だけが目的なので、インスタンスの代わりに型付きのnilを置けます。
PipeのPullは値レシーバなので、Pipe{}でも(*Pipe)(nil)でも代入できます。
いつ書くか
ライブラリで標準的なインターフェース(io.Readerなど)を実装するとき、型の直後に置くことが多いです。インターフェース側にメソッドが追加されても、実装型でコンパイルエラーになります。すべての型に必須ではなく、どこにもインターフェースへの代入がなく、契約を明示したいときの手段です。
複数のインターフェースを満たすことをまとめて宣言することもできます。
var (
_ Puller = Pipe{}
_ Pusher = Pipe{}
)