Goのinterface/structの埋め込み

(2017-01-18)

Goには継承が存在しない。その代わりstructを埋め込み、委譲することができる。

https://golang.org/doc/effective_go.html#embedding

挙動

interfaceにinterfaceを埋め込む

type I interface {
	Hoge()
}

type J interface {
	Fuga()
}

type K interface {
	I
	J
}

インタフェースKはIとJを合わせたものになる。IとJに重複する関数がある場合はエラーになる。

type L struct {
}

func (l L) Hoge() {
	fmt.Println("hoge")
}
func (l L) Fuga() {
	fmt.Println("fuga")
}
var k K
k = L{}
k.Hoge()
k.Fuga()

structにinterfaceを埋め込む

type K interface {
	Hoge()
	Fuga()
}

type M struct {
	K
}

埋め込むとm.Hoge()のように透過的にKを扱うことができるようになる。

m := M{L{}}
m.Hoge()
// m.K.Hoge() これでも呼べる

埋め込まないとこうなる。

type M struct {
	k K
}
m := M{L{}}
m.k.Hoge()

structにstructを埋め込む

type A struct {
	name string
}

type B struct {
	A
}

func (a A) hoge() {
	fmt.Println("hoge", a.name)
}

上と同様、b.nameや、b.hoge()のように扱える。 Aの関数も呼べて一見継承しているように見えるが、実際はAへの委譲となる。なのでhogeのレシーバーはA。

b := B{}
b.name = "a"
// b = B{A{name: "a"}} 
b.hoge()
// b.A.hoge()

また、Bにもhogeを実装すると、b.hoge()でこちらが呼ばれることになる。

func (b B) hoge() {
	fmt.Println("fuga", b.name)
}
b := B{}
b.name = "piyo"
b.hoge() // => fuga piyo
b.A.hoge() // => hoge piyo

継承との違い

type A struct {
	name string
}

type B struct {
	A
}

func (a A) hoge() {
	fmt.Println("hoge", a.name)
}

func (a A) fuga() {
	a.hoge()
}

// override?
func (b B) hoge() {
	fmt.Println("fuga", b.name)
}

もし、BがAを継承しているとするとb.fuga()内でhoge()を呼ぶと、オーバーライドした、Bをレシーバーとするhoge()が呼ばれ、”fuga”が出力されるはずだ。 しかし、fuga()のレシーバーはAなので、呼ばれるのはAをレシーバーとする方のhoge()となり、”hoge”が出力される。

b := B{}
b.name = "piyo"
b.fuga() // => hoge piyo

また、type Aの変数にBを代入することもできない。

用途を探る

interfaceのデフォルト実装

interfaceのデフォルトの実装を用意したstructを埋めることで、各structでは差分だけを実装すればいいようにできる。 ただデフォルト実装が他のデフォルト実装の関数を呼んでいる場合、呼び先の関数だけ実装しても元々の関数の動作は変わらないので注意。 レシーバーを意識する必要がある。

type Student interface {
	Plus(x int, y int) int
	Minus(x int, y int) int
}

type defaultStudent struct{}

func (defaultStudent) Plus(x int, y int) int {
	return x + y
}

func (defaultStudent) Minus(x int, y int) int {
	return x - y
}

type BadStudent struct {
	defaultStudent
}

func (BadStudent) Minus(x int, y int) int {
	return 0
}

type GeniusStudent struct {
	defaultStudent
}

func (GeniusStudent) Plus(x int, y int) int {
	if ans, err := strconv.Atoi(fmt.Sprintf("%d%d", x, y)); err != nil {
		fmt.Errorf("genius student fails to %d + %d", x, y)
		return 0
	} else {
		return ans
	}
}

Publicな機能の追加

Publicな関数を提供するstructを埋め込み、それを直接外からも呼べるようにする。

試しに、sync.RWMutex()を埋め込んでみた。これはゼロ値でロックしていない状態なので初期化する必要はない。 一見良さそうに見えるが、この例だとLockとUnlockが外から呼べてしまうのは意図した挙動ではない。 埋め込んだstructがどんなPublicの関数を提供しているかを理解しておく必要がある。

type Twin struct {
	num     int
	sameNum int
	sync.RWMutex
}

func (l *Twin) Set(n int) {
	l.Lock()
	l.num = n
	l.sameNum = n
	l.Unlock()
}

func (l *Twin) Check() (ok bool) {
	l.RLock()
	ok = l.num == l.sameNum
	l.RUnlock()
	return
}

func main() {

	twin := new(Twin)

	for i := 0; i < 1000; i++ {
		go twin.Set(i)
		if !twin.Check() {
			panic("broken")
		}
	}

	fmt.Println("success")

	twin.Unlock() // panic!
}

Privateな共通処理をまとめる

privateな共通処理を埋め込むことでDRYに書けるように試みる。

まず、状態を持たないような処理の場合。

func (defaultStudent) hoge() bool {
	return true
}

これはパッケージが適切に切られていれば

func hoge() bool {
	return true
}

と変わらず、埋め込む必要はないと思う。埋め込むとレシーバーから補完が効いて探しやすいかもしれないけれど。

一方、状態を持つような処理の場合。

func (n defaultStudent) counter() int {
    n.counter += 1
    return n.counter
}

これは埋め込むか、明示的にフィールドを持つかのどちらかになる。

type BadStudent struct {
	defaultStudent
}

or 

type BadStudent struct {
	d defaultStudent
}

今回の場合、defaultStudentにPublicなデフォルト実装があってそれを使うので埋め込んだ方が楽だが、 そうでなければどちらでもさほど変わらないと思う。ただ、埋め込まないと数文字増える代わりにレシーバーが明確になる(書き方を強制する)。

埋め込みは強力だけど、継承ではないし、レシーバーも分かりづらくなるので注意するべきだと思った。

参考

オブジェクト指向言語としてGolangをやろうとするとハマる点を整理してみる - Qiita