TL;DR
- ポイント1: インターフェースに構造体を埋め込むことはできない。逆に他3つはできる。
- ポイント2: "借り物"のメソッドを自分のものとして使うことができる。
- ポイント3: 他言語の継承とは異なり、埋め込み先のメンバーに影響を与えない。
- ポイント4: 埋め込み元と埋め込み先に同じフィールド名が存在するとき、埋め込み先が優先される。
はじめに
Go言語の構造体・インターフェースの埋め込み(embedding)についてeffective_goの記事とインタフェースの実装パターン #golangの記事を拝見し、簡単なプログラムを動かしながらポイントをまとめてみました。
ポイント1: 埋め込まれる × 埋め込む の関係
インターフェースに何を埋め込めるか構造体に何を埋め込めるか把握しづらかったので、そもそもできるかできないかの観点で関係を表にしました。
インターフェースを埋め込む | 構造体を埋め込む | |
---|---|---|
インターフェースに | 可 (①) | 不可 |
構造体に | 可 (②) | 可 (③) |
インターフェースに構造体は埋め込めません。インターフェースにはシグネチャを与えるものなので具象な構造体を埋め込むのはNGですね。
逆に他3つは可能です。それぞれの使い方を下記のサンプルコードで確認しました。
package main
import (
"fmt"
)
type Flyer interface {
Fly() string
}
type Runner interface {
Run() string
}
// ① インターフェースにインターフェースを埋め込む
type FlyingRunner interface {
Flyer
Runner
}
// ② 構造体にインターフェースを埋め込む
type ToriJin struct { // 鳥人
FlyingRunner
}
// ③ 構造体に構造体を埋め込む
type ShinJinrui struct { // 新人類
*ToriJin
}
// FlyingRunnerインターフェースを実装する型
type RealToriJin struct{}
func (r RealToriJin) Fly() string { return "Fly!" }
func (r RealToriJin) Run() string { return "Run!" }
func main() {
aRealToriJin := &RealToriJin{}
// ② 構造体ToriJinにFlyingRunnerインターフェースを
// 実装しているRealToriJinの変数を埋め込む。
aToriJin := &ToriJin{
FlyingRunner: aRealToriJin,
}
// ③ 構造体ShinJinruiに構造体Torijinの変数を埋め込む。
aShinJinrui := &ShinJinrui{
ToriJin: aToriJin,
}
fmt.Println(aShinJinrui.Fly()) // Fly!
fmt.Println(aShinJinrui.Run()) // Run!
}
埋め込むことによって何が起こっているか、以降のポイント2と3で記述します。
ポイント2: 埋め込みのメリット
そもそも埋め込みができて嬉しいのは、わざわざ自身の型でメソッドを実装しなくても"借り物"のメソッドを使うことができるDRYの点です。
以下のサンプルコードではShinJinrui2
という構造体が*grasshopper
型のHighJump
メソッドをそのまま借りることでHighJumpRunner
インターフェースを実装できていることを示しています。
package main
import "fmt"
type HighJumpRunner interface {
HighJump() string
Run() string
}
// grasshopper はバッタのこと
// 高く飛ぶ能力がある
type grasshopper struct{}
func (g *grasshopper) HighJump() string {
return "High Jump!"
}
// ShinJinrui2 は*grasshopperの能力を
// 構造体埋め込み(③)により"そのまま"借りる
type ShinJinrui2 struct { // 新人類2
*grasshopper
}
func main() {
aGrassHopper := &grasshopper{}
aShinJinrui2 := &ShinJinrui2{
grasshopper: aGrassHopper,
}
if _, ok := interface{}(aShinJinrui2).(HighJumpRunner); ok {
fmt.Println("ShinJinrui2はHighJumpRunnerインターフェースを実装しています。")
}
fmt.Println(aShinJinrui2.HighJump()) // High Jump!
}
以下のように埋め込まない(そのまま借りない)場合、型ShinJinrui2
のメソッドとしてHighJump
を定義し、その中で*grasshopper
のものを呼ぶ実装を作る必要があります。
type ShinJinrui2 struct { // 新人類2
ghopper *grasshopper
}
func (sj *ShinJinrui2) HighJump() string {
return sj.grasshopper.HighJump()
}
ポイント3: Goの埋め込みはサブクラス化とは異なる
埋め込みはあくまでも"借りているだけ"で埋め込み元のオブジェクトのメソッドとして実行されます。
これは埋め込み先の構造体が埋め込み元のメソッドを実行しても埋め込み先オブジェクトには影響を与えないことを意味しています。
これが他の一般的なオブジェクト指向言語が提供する継承とは異なるポイントです。(Go言語には継承がありません。)
クラス継承の問題点としてあがる、親クラスの実装が子クラスのオブジェクトに影響を与えるといったことがGoの埋め込みではありません。
以下がそれを示すサンプルコードです。
package main
import (
"fmt"
)
// Status は健康状態を意味する
type Status int
const (
// Good is 良好 status
Good Status = iota
// Tired is 疲れている status
Tired
)
func (s Status) String() string {
switch s {
case Good:
return "Good!"
case Tired:
return "Tired..."
default:
return ""
}
}
type poorGrasshopper struct {
status Status // poorGrasshopperには健康状態がある
}
func (g *poorGrasshopper) HighJump() {
fmt.Println("High Jump!")
g.status = Tired // 飛ぶと疲れてしまう
}
type ShinJinrui3 struct { // 新人類3
status Status // ShinJinrui3も健康状態がある
*poorGrasshopper // 構造体の埋め込み(③)
}
func main() {
aPoorGrasshopper := &poorGrasshopper{
status: Good,
}
aShinJinrui3 := &ShinJinrui3{
status: Good,
poorGrasshopper: aPoorGrasshopper,
}
aShinJinrui3.HighJump()
// poorGrasshopperの方はステータスが変わるが
// メソッドを借りているだけのShinjirui3のステータスは影響されない
fmt.Println("aPoorGrasshopper is", aPoorGrasshopper.status) // Tired
fmt.Println("aShinJinrui3 is", aShinJinrui3.status) // Good
}
ポイント4: 埋め込み元と埋め込み先のメソッド名重複時の挙動
埋め込み元と埋め込み先に同じ名前のメソッド・フィールド名が存在するとき、コンパイルエラーにはならずもともとの埋め込み先のものが優先されて呼ばれます。
これにより、埋め込み元のメソッド呼び出しを埋め込み先でも行いながら、追加としてカスタマイズした機能を実装することが可能になります。
以下はメソッド名重複時の挙動を確認するだけのサンプルコードです。
package main
import (
"fmt"
"log"
)
// dolphin はイルカ
// 水中に潜る能力がある
type dolphin struct{}
func (g *dolphin) Dive() string {
return "Dolpin Dive!"
}
// ShinJinrui4 はdolphinの能力を
// 構造体埋め込み(③)により"そのまま"借りる
type ShinJinrui4 struct {
*dolphin
}
// ShinJinrui4はdolphinの能力の発動に加えて自身の能力を発動できる。
func (sj *ShinJinrui4) Dive() string {
return sj.dolphin.Dive() + " and ShinJinrui Dive!"
}
func main() {
aDolphin := &dolphin{}
aShinJinrui4 := &ShinJinrui4{
dolphin: aDolphin,
}
// 埋め込こんだ*dophinにも重複した名前のメソッド、Dive
// があるがShinJinru4自身のものが優先されて呼ばれる
fmt.Println(aShinJinrui4.Dive()) // Dolpin Dive! and ShinJinrui Dive!
}
【不明点】 重複した名前が許容されるケースの話が再現できない
effective_goには下記のように、外部からアクセスされなければ重複を許容とあるのですが、実際に下記のサンプルコードのようにメソッドを定義するとtype Job has both field and method named Logger
のコンパイルエラーがビルド時に発生しまい、説明の意味を把握できていません。
Second, if the same name appears at the same nesting level, it is usually an error; it would be erroneous to embed log.Logger if the Job struct contained another field or method called Logger. However, if the duplicate name is never mentioned in the program outside the type definition, it is OK. This qualification provides some protection against changes made to types embedded from outside; there is no problem if a field is added that conflicts with another field in another subtype if neither field is ever used.
package main
import (
"fmt"
"log"
)
type Job struct {
*log.Logger
}
func (j *Job) Logger() string { // コンパイルエラー "type Job has both field and method named Logger"
return "A method name, dolphin of ShinJinru5"
}
func main() {
fmt.Println("Hello, playground")
}