💡

オレの書くGoは間違っていた

2022/08/30に公開
2

Go言語には"Accept interfaces, return structs"というお作法がある。
このお作法の存在に気づいていなかったし、そうでなくても正しく動いてくれていた。
Goを3年ほど書いてきてようやくお作法を理解したので紹介したい。

https://bryanftan.medium.com/accept-interfaces-return-structs-in-go-d4cab29a301b

これまでのぼくの書き方

以下にこれまでぼくが書いていたコードのinterface定義の例とその実装の例を示す。

Example structはExample interfaceの定義を満たしており、DoSomething関数を持つ。
interfaceのありがたみはinterfaceさえ参照すれば、それを満たすstructがどんな関数を持っているのか知ることができる点だとばかり思っていた。
interfaceを引数に取る関数に依存性の注入などを行ったときに返り値と引数の型が異なってしまうため、NewExample関数の返り値はinterfaceを返せばいいものだと思っていた。
しかも、structがinterfaceの実装を満たしていなかった場合におまじないの行とNewExampleの行で同じエラーが発生していた。(意味あるのかな?と思っていた)

example.go
package example

var _ ExampleInterface = (*Example)(nil) // おまじない

type ExampleInterface interface {
  DoSomething() error
}

type Example struct {}

func NewExample() ExampleInterface {
  return &Example{}
}

func (e *Example) DoSomething() error {
  return nil
}

これが正しいGoの書き方だ

なんとGo公式にinterfaceを関数の返り値にするのはダメだと書かれており、それに従うと以下のような実装になる。
NewExample関数がstructを返すようになったので、おまじない行と重複したエラーが発生することはなくなった。これでおまじないが成立するわけだ。
ただ、この方法だと前述のように依存性の注入を行ったときにNewExample*Exampleを返し、受け取る引数はExampleInterfaceを必要としているので型が異なってしまう。

https://github.com/golang/go/wiki/CodeReviewComments#interfaces

example.go
- func NewExample() ExampleInterface {
+ func NewExample() *Example {
  return &Example{}
}

これを解決するためにwireなどのDIツールで以下のように記述する。
これでwire.Build関数にNewExampleSetを渡せばNewExample関数の返り値が*ExampleでもExampleInterfaceを受け取りたい関数とマッチしてくれる。

example.go
+ var NewExampleSet = wire.NewSet(
+   wire.Bind(new(ExampleInterface), new(*Example)),
+   NewExample,
+ )

まとめ

Accept interfaces, return structsというお作法について理解した。
関数の引数にinterfaceを受け取ると、定義を満たすstructであればすり替え可能なためにテスト時のmockのしやすさにつながる。

Goの魅力はこれに限らないと感じるので、もっと深く知っていきたい。

Discussion

rihibrihib

この記事についてGoのコントリビュータの方がツイートしてます。@tweet

Pranc1ngPegasusPranc1ngPegasus

お返事が遅くなってしまい申し訳ございません。教えていただきありがとうございます。
コメントいただいた件について記事冒頭にメッセージを表示させていただきました。