Go 構造体埋め込み
JavaやC++などの伝統的なオブジェクト指向言語では、「継承(Inheritance)」がコード再利用の主な手段です。しかし、Go言語には extends キーワードが存在しません。
これは、Go言語が設計思想として「継承よりもコンポジション(合成)を優先する」という哲学を極めて重視しているためです。構造体埋め込み(Struct Embedding)を利用することで、シンプルな構造体を積み木のように組み合わせて複雑な構造体を構築できます。この手法は、深い継承ツリーによる硬直化や脆弱性を回避するだけでなく、コードに高い柔軟性とメンテナンス性をもたらします。
本章では、この強力なコード再利用メカニズムを完全にマスターしましょう。
1. 構造体埋め込みとは?
構造体埋め込み(コンポジション)とは、ある構造体を別の構造体のフィールドとして含めることを指します。これは「自動車はエンジンを持っている(has-a)」という関係であり、「自動車はエンジンである(is-a)」という継承関係とは異なります。
1.1 具名埋め込み
最も基本的な方法は、埋め込む構造体に明確なフィールド名を付けることです。
package main
import "fmt"
type Address struct {
Street string
City string
}
type Person struct {
Name string
Age int
Address Address // 具名埋め込み:Address はフィールド名、型も Address
}
func main() {
person := Person{
Name: "John Doe",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "Anytown",
},
}
// フルパスでアクセスする必要がある
fmt.Println("都市:", person.Address.City)
}この例では、Person は Address を保持しています。内部のデータにアクセスするには、person.Address.City のように完全な階層パスを辿る必要があります。
2. 匿名埋め込みとプロモーションの魔法
Go言語には、より魔法のような埋め込み方式が用意されています。それが「匿名埋め込み」です。これはフィールド名を記述せず、埋め込む構造体の型名のみを記述します。
このようにすると、埋め込まれた構造体内部のすべてのフィールドが、外部の構造体に「昇格(Promoted)」されます。
2.1 フィールド昇格の例
package main
import "fmt"
type Address struct {
Street string
City string
}
type Person struct {
Name string
Age int
Address // 匿名埋め込み:型名のみを記述
}
func main() {
person := Person{
Name: "John Doe",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "Anytown",
},
}
// ✨ 魔法:Address をスキップして、直接 City や Street にアクセスできる!
fmt.Println("都市:", person.City) // フィールド昇格
fmt.Println("街道:", person.Street) // フィールド昇格
// もちろん、フルパスも引き続き有効
// fmt.Println(person.Address.City)
}2.2 具名埋め込み vs. 匿名埋め込み の比較
| 特性 | 具名埋め込み (Addr Address) | 匿名埋め込み (Address) |
|---|---|---|
| 定義方法 | 明確なフィールド名と型を指定する。 | 型名のみを指定する。 |
| アクセスパス | フル階層(例:p.Addr.City)が必須。 | 直接アクセス可能(例:p.City)。 |
| ユースケース | 同じ型のサブ構造体を複数含む場合(例:HomeAddr, WorkAddr)。 | 「継承」に近い振る舞いをシミュレートし、内部の属性や機能を直接公開したい場合。 |
3. メソッドの昇格
匿名埋め込みの魅力はフィールドだけではありません。埋め込まれた構造体が持つメソッドも同様に、外部の構造体へと昇格されます。
これにより、外部の構造体は内部構造体のすべての振る舞いを「自動的」に獲得します。
package main
import "fmt"
type Engine struct {
Cylinders int
}
// Start は Engine 型のメソッド
func (e Engine) Start() {
fmt.Println("エンジン起動。シリンダー数:", e.Cylinders)
}
type Car struct {
Make string
Model string
Engine // 匿名埋め込み Engine
}
func main() {
car := Car{
Make: "Toyota",
Model: "Camry",
Engine: Engine{Cylinders: 4},
}
// Car 自身に Start メソッドは定義されていませんが、Engine を匿名埋め込みしているため、
// その機能を直接利用できます!
car.Start()
}4. 名前衝突のハンドリング
外部の構造体と埋め込まれた内部の構造体に同名のフィールドやメソッドがある場合、どうなるでしょうか?
Goのルールは非常にシンプルです。「外側が常に優先される(最近接の原則)」。
外側の同名フィールドやメソッドが、内側のものを「シャドウイング(遮蔽)」します。
package main
import "fmt"
type Engine struct {
Power int
}
type Car struct {
Make string
Engine // 匿名埋め込み
Power int // ⚠️ 外側にも Power という名前のフィールドがある!
}
func main() {
car := Car{
Make: "Toyota",
Engine: Engine{Power: 150},
Power: 200,
}
// デフォルトでは外側の Power (200) にアクセスされる
fmt.Println("自動車の公称パワー:", car.Power)
// シャドウイングされた内側の Power にアクセスしたい場合は、フルパスを記述する
fmt.Println("実際のエンジンパワー:", car.Engine.Power)
}このメカニズムを利用することで、埋め込まれた構造体のデフォルトの振る舞いを非常に簡単に「オーバーライド(上書き)」することができます。
5. 多重埋め込みと実践的な応用
Goでは、複数の構造体を同時に一つの構造体へ匿名埋め込みすることが可能です。これにより、必要なパーツを選ぶように、異なる機能モジュールを一つの主体に組み合わせることができます。
例えば、ビジネスサービスに動的にログ機能を追加する場合:
package main
import "fmt"
// ログモジュール
type Logger struct {
Prefix string
}
func (l Logger) Log(message string) {
fmt.Println(l.Prefix + ": " + message)
}
// コアビジネスサービス
type Service struct {
Name string
Logger // ログモジュールの機能を注入
}
func (s Service) Run() {
// 昇格された Log メソッドを直接呼び出す
s.Log("サービスの起動を開始します...")
fmt.Println("サービス", s.Name, "は全速力で稼働中です")
s.Log("サービスが終了しました。")
}
func main() {
service := Service{
Name: "AuthService",
Logger: Logger{Prefix: "[権限チェック]"},
}
service.Run()
}この例では、Service 自身がログ出力のロジックを書く必要はありません。Logger を「コンポジション」するだけで、プレフィックス付きのログを出力する超能力を即座に獲得しました。これこそがコンポジションの芸術です。