インターフェース型
インターフェース型は、具体的な型を指定せず呼び出せるメソッドの条件だけを書いた型です。条件を満たす値なら、中身の型が違っても同じ変数に入れられます。
type Shape interface {
Area() float64
}メソッドを1つも宣言しないインターフェースは空インターフェースと呼ばれ、任意の型(インターフェース型を含む)を格納できます。anyは空インターフェースの別名です。
インターフェース型の変数の中身
インターフェース型の変数には、コンパイル時に決まる静的型と、実行時に決まる動的型・動的値が入ります。
静的型(static type)
コンパイル時に確定する型のことです。var a Shapeと定義されていればShapeが静的型です。実行中にaの中身が*Rectでも別の型でも、静的型はShapeのままです。
動的型(dynamic type)
実行時にそのインターフェースの中に実際に格納されている具体的な型のことです。aに*Rectを入れれば動的型は*Rectとなり、別の値を入れ直せば動的型も変わります。何も代入されていない初期状態では動的型も nil です。
動的値(dynamic value)
実行時にそのインターフェースの中に実際に格納されているデータのことです。*Rectを入れていれば、データが格納されているアドレスへのポインタ(またはnil)が動的値です。
暗黙の実装
オブジェクト指向な言語とは異なり、implementsのような宣言はありません。型がインターフェースが要求するメソッドをすべて備えていれば、宣言なくそのインターフェースを実装したとみなされます。
type Shape interface {
Area() float64
}
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 {
return r.W * r.H
}
var a Shape = Rect{W: 3, H: 4} // RectがShapeのメソッドを持つので代入可能値型Tのメソッド集合にはレシーバがTのメソッドが入ります。ポインタ型*Tのメソッド集合には、レシーバがTまたは*Tのメソッドが入ります。次の例では、値型Rectのメソッド集合にはAreaだけが含まれ、ポインタ型*Rectのメソッド集合にはAreaとScaleが含まれます。
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
func (r *Rect) Scale(f float64) { r.W *= f; r.H *= f }メソッド集合が異なるため、要求するメソッドによって代入可否が変わります。
type Shape interface { Area() float64 }
type Scaler interface { Scale(float64) }
var a Shape = Rect{} // OK: RectにAreaがある
// var s Scaler = Rect{} // NG: RectにScaleがない
var s Scaler = &Rect{} // OK: *RectにScaleがあるnilの扱い
インターフェース型の変数では、動的型と動的値の両方がnilのときだけnilと等価になります。
インターフェース型の変数のゼロ値は、動的型も動的値もnilです。
var a Shape
fmt.Println(a == nil) // truenilポインタをインターフェースに入れると、ポインタ自体はnilでもインターフェースとしては空ではないためnilと等価になりません。
var p *Rect
fmt.Println(p == nil) // true: ポインタ変数としてはnil
var a2 Shape = p
fmt.Println(a2 == nil) // false: 動的型*Rectがあるインターフェースの埋め込み
インターフェース定義に別のインターフェース型名を並べると埋め込みになります。外側のインターフェースは、内側のインターフェースが要求するメソッドをすべて引き継ぎます。
type Shape interface {
Area() float64
}
type Perimer interface {
Perimeter() float64
}
type Measurer interface {
Shape
Perimer
}
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return 3.14159 * c.R * c.R }
var m Measurer = Rect{} // OK
var a Shape = Rect{} // OK
var p Perimer = Rect{} // OK
// var c Measurer = Circle{} // NG: Perimeterがない型アサーション
インターフェース値から具体的な値を取り出すには型アサーションを使います。s, ok := v.(T)の2値形式で取り出し、動的型がTでなければokがfalseになります。
var v any = Rect{W: 3, H: 4}
r, ok := v.(Rect)
if !ok {
// 動的型がRectでない
return
}
// ここではrをRectとして使えるokを受け取らないs := v.(T)は、動的型がTでないときにパニックになります。ユーザー入力や外部データから復元するときは、必ず2値形式で分岐します。
複数の動的型を分岐するときはswitch v := x.(type)の型スイッチが使えます。動的型に応じてcaseが選ばれ、どのcaseにも当てはまらなくてもパニックになりません。
func show(x any) {
switch v := x.(type) {
case Rect:
fmt.Println("rect:", v)
case Circle:
fmt.Println("circle:", v)
default:
fmt.Println("other type")
}
}インターフェース実装の確認
コンパイラは代入が起きたときにメソッド集合を見てインターフェースを満たすか判断します。型定義の近くにインターフェース型の変数への代入を書いておくと、メソッドの追加漏れやレシーバ型の取り違えをビルド時に検知できます。
type Shape interface { Area() float64 }
type Scaler interface { Scale(float64) }
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 {
return r.W * r.H
}
func (r *Rect) Scale(f float64) {
r.W *= f
r.H *= f
}
var _ Shape = Rect{} // OK
// var _ Scaler = Rect{} // NG: RectにはScaleがない
var _ Scaler = (*Rect)(nil) // OK左辺の_は値を使わない時に用いる識別子です。(*Rect)(nil)は実体を作らず、右辺の型が*Rectであることだけをコンパイラに伝えます。