Goの埋め込みについて
はじめに
Goにはオブジェクト指向言語みたいな継承が無いから、何かしら実装を再利用する別の手段が必要だ。だって毎回実装するの面倒だし。んで、この実装の再利用をGoで実現するのが埋め込み(embedding)という仕組みであるらしい。
埋め込みとは
構造体の定義の中に、フィールド名を書かずに構造体名やインターフェース名を書くと、それらを埋め込むことができる。このとき、フィールド名は型の名前と同じになり、埋め込まれた構造体やインターフェースのフィールドやメソッドは埋め込み先の構造体に定義されているように、扱うことができる。フィールド名(=型名)を使ったアクセスもできる。
package main
import (
"fmt"
)
type T struct {
T1 // 構造体T1を埋め込み
}
// 構造体T1はフィールドiとメソッドnumを持つ
type T1 struct {
i int
}
func (t1 *T1) num() int {
return t1.i
}
func main() {
// TのフィールドT1として初期化できる
t := T{T1: T1{i: 1}}
// 構造体Tから直接iとnumにアクセスできる
fmt.Printf("t.i=%d\n", t.i) // => t.i=1
fmt.Printf("t.num()=%d\n", t.num()) // => t.num()=1
// フィールドT1経由のアクセスもできる
fmt.Printf("t.T1.i=%d\n", t.T1.i) // => t.T1.i=1
fmt.Printf("t.T1.num()=%d\n", t.T1.num()) // => t.T1.num()=1
}
この場合、埋め込まれた構造体T1のフィールドiやメソッドnumは構造体Tに定義されているかのように参照することができる。実際には埋め込まれた構造体T1のフィールドiが参照されていたり、メソッドnumが呼ばれているわけで、自動的に委譲されていると言ってもさほど間違いではなさそうに思える。
複数の構造体を埋め込んだ際に定義されたフィールドやメソッドの名称がかぶると、呼び出そうとしたときにどっちかわからんというエラーになる。定義するだけならエラーにならないので、フィールド名(=型名)をつけてアクセスすることでエラーを回避できる。
インターフェースの埋め込み
構造体にインターフェースを埋め込むと、インターフェースに定義されたメソッドを直接呼ぶことができるようになる。その際に呼ばれるメソッドはインターフェース名のフィールドに格納されたもののメソッドになる。
package main
import (
"fmt"
)
type T struct {
ihoge // インターフェースihoge埋め込み
}
// インターフェースihogeはメソッドnumを持つ
type ihoge interface {
num() int
}
// T1はフィールドiとメソッドnumを持つ
type T1 struct {
i int
}
func (t1 *T1) num() int {
return t1.i
}
func main() {
// Tのフィールドihogeとして初期化できる
t := T{ihoge: &T1{i: 1}}
// fmt.Printf("t.i=%d\n", t.i) // iは無いのでエラー
fmt.Printf("t.num()=%d\n", t.num()) // => t.num()=1
}
これはちょっと興味深い。ihogeに入っている構造体を差し替えれば、動的に構造体Tのメソッドの挙動を変えられると言っている。
試してみよう。
package main
import (
"fmt"
)
type T struct {
ihoge // ihogeインターフェース埋め込み
}
// ihogeインターフェースはcalcメソッドを持つ
type ihoge interface {
calc(a, b int) int
}
// ihogeインターフェースに対応した構造体Add
type Add struct{}
func (t *Add) calc(a, b int) int {
return a + b
}
// ihogeインターフェースに対応した構造体Mul
type Mul struct{}
func (t *Mul) calc(a, b int) int {
return a * b
}
func main() {
t := T{}
t.ihoge = &Add{}
fmt.Printf("t.calc(3, 4)=%d\n", t.calc(3, 4)) // => t.calc(3, 4)=7
t.ihoge = &Mul{}
fmt.Printf("t.calc(3, 4)=%d\n", t.calc(3, 4)) // => t.calc(3, 4)=12
}
確かに変わった。実行中に挙動を変更する手法として面白いが、イマイチ有用な使い道が思いつかない。
パッケージをまたぐ場合
別パッケージで定義された構造体を埋め込む場合、パッケージ名から名前を書くことになり、定義されるフィールド名はパッケージ名を除いたものになる。このフィールドの型はパッケージ名から始まるので、初期化のリテラルの書き方に気を付ける。
package main
import (
"fmt"
"myproject/package1"
)
type T struct {
package1.T1 // package1の構造体T1埋め込み
}
func main() {
t := T{T1: package1.T1{I: 1}}
fmt.Printf("t.I=%d\n", t.I) // => t.I=1
}
package package1
type T1 struct {
I int
}
大文字から始まるフィールド名であれば公開フィールドなので参照できるが、小文字から始まると非公開フィールドになって参照できなくなる。非公開フィールドが混ざった場合はNew何某という名前の関数を用意して初期化するか、初期値でいいように作っておく感じになる。
おしまい
インターフェースと埋め込みはGoの特徴的な機能なので、このあたりの話は検索すると大量に出てくる。ちょっと大きいものを作ろうとしたらこの2つを駆使してスマートに設計することになるのだろう。
世の中に存在する膨大なオブジェクト指向の技法とは違うが、既に色んなものが作られていることを考えると、たいがいのものを作るのに不都合はなさそうに思える。
Discussion