1つの例で見る埋め込み
型はあるが名前のないフィールドが、埋め込まれたフィールドです。埋め込まれた型のフィールドとメソッドが、外側の型から直接アクセスできるようになります。
出力:
Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}
埋め込まれたフィールドの名前は、その型名の User です。リテラルで参照するとき(User: User{...})や、内側の値そのものが必要なとき(a.User)はこの名前を使います。昇格されたフィールドを、リテラルの中で短い名前で設定することはできません。Admin{Name: "Ana"} は unknown field Name in struct literal of type Admin で失敗します。
昇格されたメソッドはインターフェースを満たす
昇格によって、埋め込まれた型のメソッドが外側の型のメソッドセットに加わります。そのため外側の型は、内側の型が満たすすべてのインターフェースを満たします。
メソッドセットのルールは、埋め込まれた型に従います。T を埋め込むと、T の値レシーバーのメソッドは外側の値とポインタの両方に昇格されますが、*T のポインタレシーバーのメソッドは外側のポインタにだけ昇格されます。*T を埋め込むと、両方のセットが両方に昇格されます。迷ったらポインタを埋め込むか、外側の型をポインタで使いましょう。
ポインタの埋め込みにはコストがあります。ポインタがnilになりうることです。Logger のない Service{} はコンパイルでき、そのとき svc.Log(...) はpanicします。
埋め込みは継承ではない
埋め込みはサブクラス化のように見えますが、JavaやPythonと違う振る舞いが3つあります。
外側の型は内側の型ではない。 Admin は User ではありません。User を受け取る関数は Admin を受け付けないので、a.User を渡します。
仮想ディスパッチはない。 昇格されたメソッドは埋め込まれた値の上で実行され、外側の型については何も知りません。別のメソッドを呼ぶと、外側の型が同じ名前のメソッドを定義していても、自分の型の版を呼びます。
出力:
woof
The animal says ...
クラスベースの言語なら、Speak は「woof」と表示するでしょう。Goでは d.Speak() は d.Animal.Speak() の省略形で、その呼び出しのレシーバーは Animal です。型によって振る舞いを変えたいなら、インターフェースを使います。それを必要とするコードに Sounder を渡します。
内側の型は自分が埋め込まれていることを知らない。 super はありません。昇格されたメソッドを拡張するには、外側の型に同じ名前のメソッドを定義し、内側のメソッドを明示的に呼びます。
func (a Admin) Greet() string {
return a.User.Greet() + " (admin)"
}
名前の衝突とシャドーイング
外側の型のフィールドやメソッドは、上の Dog.Sound のように、同じ名前の昇格されたものを隠します。浅いほうの名前が勝ちます。
同じ深さにある2つの埋め込まれた型が同じ名前を昇格させると、その名前はあいまいになります。そのあいまいな名前を使わない限り、プログラムはコンパイルできます。
完全なパスを使うか、外側の型に ID を定義して衝突を解消します。
インターフェースの埋め込み
インターフェースは他のインターフェースを埋め込めます。標準ライブラリはこの方法で大きなインターフェースを作っています。
type ReadWriter interface {
Reader
Writer
}
構造体もインターフェースを埋め込めます。すると構造体は、そのフィールドに格納した値を通してインターフェースを満たし、気になるメソッドだけを上書きできます。ほとんどコードを書かずにデコレーターパターンになります。
明示的な c.Reader.Read(p) が必要です。Read の中で c.Read(p) と書くと、自分自身を永遠に呼び続けます。
テスト用の偽物にインターフェースを埋め込むのはよくある近道です。大きなインターフェースを埋め込み、テストに必要な1つのメソッドだけを実装して、残りはそのままにします。実装していないメソッドを呼ぶとnilポインタの参照外しでpanicしますが、テストではたいていそれが望ましい挙動です。
実際によくある用途:sync.Mutexの埋め込み
type Stats struct {
sync.Mutex
hits map[string]int
}
func (s *Stats) Hit(page string) {
s.Lock()
defer s.Unlock()
s.hits[page]++
}
読みやすくはなりますが、Lock と Unlock も Stats のAPIの一部として公開されるので、どの呼び出し側でも構造体をロックできてしまいます。パッケージの外で使われる型なら、名前付きのフィールド(mu sync.Mutex)にすればロックを非公開にできます。このトレードオフはすべての埋め込みに当てはまります。埋め込まれた型が公開しているものはすべて、あなたの型の公開された表面の一部になります。
よくある間違い
- 仮想ディスパッチを期待する。 昇格されたメソッドが外側の型の上書きを呼ぶことはありません。
- リテラルで昇格されたフィールドを設定する。 埋め込まれた型の名前を使います:
Admin{User: User{Name: "Ana"}}。 - nilの埋め込みポインタ。 埋め込まれた
*Tやインターフェースは、メソッドを呼ぶ前に設定しておく必要があります。 - 意図しないAPIの表面。 埋め込むと、内側の型の公開されたメソッドがすべてあなたの型で公開されます。
よくある質問
Goの構造体の埋め込みとは何ですか?
名前を付けずに型だけでフィールドを宣言することです:type Admin struct { User; Level int }。埋め込まれた User のフィールドとメソッドが昇格されるので、Admin に対して a.Name や a.Greet() が直接使えます。埋め込まれた値も普通のフィールドであることに変わりはなく、a.User でアクセスできます。
Goに継承はありますか?
ありません。Goにはクラスもサブタイプの継承もありません。埋め込みはコンポジションによるコードの再利用を提供します。外側の型は内側の型のメソッドを得ますが、Admin は User ではありません。User が必要な場所に Admin を渡すことはできず、昇格されたメソッドが外側の型のメソッドを呼び返すこともできません。Goのポリモーフィズムはインターフェースから生まれます。
Goで埋め込まれた構造体はどう初期化しますか?
複合リテラルの中で、埋め込まれたフィールドをその型名で指定します:Admin{User: User{Name: "Ana"}, Level: 2}。昇格されたフィールドをリテラルで直接設定することはできません。Admin{Name: "Ana"} は unknown field Name in struct literal of type Admin で失敗します。
Goで構造体にインターフェースを埋め込めますか?
埋め込めます。すると構造体は埋め込まれた値を通してそのインターフェースを満たし、個々のメソッドを上書きできます。デコレーターやテスト用の偽物でよく使われます。埋め込まれたインターフェースのフィールドがnilだと、上書きしていないメソッドを呼んだときにnilポインタの参照外しでpanicします。