Goは継承の概念がないので、オブジェクト指向的な振る舞いを期待して書くとあれ?ってことがあります。
Goの継承っぽい書き方は何ができるの?できないの?というのを調べてみた備忘です。
できること
先に書いたように、継承っぽい書き方はできるのです。
たとえば、猫のような振る舞いをするクラスを書いてみます。
//親クラス
//インターフェースSpeaker
type Speaker interface {
Call()
}
//構造体Cat
type Cat struct {
}
//Catの振る舞いを定義
func (c Cat) Call() {
fmt.Println("にゃー")
}
クラスが無いので、構造体にインターフェースを埋め込むことで継承のように扱おうとします。
それでは、猫クラスをタマに継承させてみましょう。
//子クラス
//Cat構造体を継承
type Tama struct {
Cat
}
タマは猫クラスを継承しているので、にゃーと鳴くはずです。
func main() {
var t Speaker
t = Tama{
}
t.Call()
}
> go run main.go
にゃー
にゃーと鳴きました。
この調子で、同じく猫クラスを継承するひろしも作ってみます。
ひろしはにゃーと鳴かずポーツマス!を2回言うので、Voice()を再定義します
type Hiroshi struct {
Cat
}
//子クラスのCall()を再定義
func (h Hiroshi) Call() {
fmt.Println("ポーツマス!ポーツマス!")
}
ひろしはポーツマス!ポーツマス!と鳴くはずです。
func main() {
var h Speaker
h = Hiroshi{
}
h.Call()
}
> go run main.go
ポーツマス!ポーツマス!
Goではこのような形で継承を書くことができます。
できないこと
一見うまくいっているように見えますが、期待通りに動かないものがあります。
親クラスのメソッドが子クラスのメソッドを呼ぶ入れ子構造の場合です。
やってみましょう。
猫クラスに、Call()を呼ぶだけのCry()メソッドを作ります。
タマがCry()するとにゃーと鳴きます。
type Speaker interface {
Call()
Cry()
}
func (c Cat) Cry() {
c.Call()
}
t.Cry()
にゃー
ひろしがCry()するとどうなるでしょう。
ポーツマス!ポーツマス!と鳴くと思いますよね。
ところがならないのです。
h.Cry()
にゃー
Cry()を呼んだときのレシーバーを見てみると、分かりやすいかも知れません。
func (c Cat) Cry() {
fmt.Printf("レシーバー:%p\n", c)
c.Call()
}
h.Cry()
レシーバー:%!p(main.Cat={})
にゃー
hiroshiをレシーバーとしてCry()を呼んでいるのに、親クラスを呼んだことになっています。
構造体にインターフェースを埋め込んでもインターフェースそのものが変化するわけではなく、そもそも親子関係など無いのです。
ひろしがCry()してもレシーバーが親クラスなので、子クラスのCall()を呼びに戻ったりはしないってことです。
まとめ
インターフェースの埋め込みは継承っぽく見えるけど、メソッドが再利用できるだけで継承できるわけじゃないのです。
参考にさせていただいた記事
埋め込み型としての構造体 ( Goは継承を使わない )
Goのinterfaceがわからない人へ
Goで学ぶポインタとアドレス