エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

Goのinterfaceの使いかた 基礎編

エムスリーエンジニアリンググループ/BIRの滝安(@juntaki)です。 BIRはビジネスインテリジェンス&リサーチの略で、そこでは医療従事者の会員向けアンケートをベースに、製薬会社へのマーケティング支援を提供する事業を行っています。BIRではエムスリーではじめてGoを導入し、今ではほとんどの新規システムをGoで立ち上げています。(一部でKotlinもつかっています)

Goでは、interfaceの使い方を知ることで、依存関係、コンポーネントの責務を整理した設計を考えることができます。 この記事ではGo導入当初にメンバに説明した、interfaceの基本的な使い方やTipsを整理していきます(すでに、Goをよく知っている人には当たり前すぎるかもしれません)。

また、この記事の後にBIRのメンバーがGo関連記事をいろいろ書く予定となっています!

interfaceの使い方

interfaceとはそれとして呼び出させる関数のシグネチャを定義したもので、たとえば次のように定義できます。 これは標準のioパッケージに含まれている定義です。Goで迷ったら標準パッケージのお作法を見に行くとだいたい正解が書いてあります。

type Reader interface {
    Read(p []byte) (n int, err error)
}

定義には満たすべき関数のシグネチャを書きます。ReaderはReadが実装されていれば満たせることがわかります。 また、intefaceの命名規則は"-er"のようなsuffixを持つべき、とされていて標準パッケージはそのようなお作法で書かれています。 命名規則やinterfaceの名からもわかるように、この機能はinterfaceを満たすものが何ができるのかを規定するものです。

さて、実際にこのinterfaceを満たすstructをつくってみます。処理は一切ないので実行するとpanicしますが、コンパイルは通ります。 これで、*Targetは、Readerを満たしていることになります。ここまでの実装は、エディタの機能か、implを使うと生成できます。

type Target struct{
    Value string
}

func (t *Target) Read(p []byte) (n int, err error) {
    panic("implement me")
}

もう1つの方法はinterfaceを埋め込んでしまうことです。この方法は、他の同じinterfaceを実装したstructに処理を移譲するケース、もしくは絶対にReadを呼び出さないようなケース(たとえばテストコード)で使います。 この書き方は、実装無しでコンパイルが通るようになり、実行時に落ちて実装忘れに気づくパターンになりやすいので注意が必要です。

type Target2 struct {
    io.Reader
}

次に、2つのinterfaceを同時に満たす場合を考えてみます。こちらの例もioパッケージに含まれるReadCloserです。 ReaderとCloserを埋め込んだinterfaceとすることで、ReadCloserを満たしているstructは自動的にReaderも、Closerも満たしている事になります。もし、何度もinterfaceを作って同じシグニチャを書いている場合は、何か整理を間違えている可能性が高いです。組み合わせられる小さな機能ごとにinterfaceを切り、それぞれに適切な名前を付けるように整理すると、良い設計へ自然に1歩近づきます。

type Closer interface {
    Close() error
}

type ReadCloser interface {
    Reader
    Closer
}

コンストラクター、戻り値と引数

Goはコンストラクターという具体的な構文が存在しているわけではなく、そういった機能、つまり、structを初期化して返すような関数は、"New-"というprefixがついた関数であるべきとされています。 戻り値としてそのまま初期化したstructを返しても良いのですが、interfaceを返すようにするとより高度なカプセル化が実現できます。

func NewTarget() io.Reader {
    return &Target{}
}

つまり、このコンストラクターを使う側のコードを書く開発者は、戻り値をTargetとして扱うことは期待されていないというメッセージを受け取ることができるのです。 次の例のように、戻り値をTargetと思ってPublicなフィールドにアクセスしようとしても、コンパイルエラーになります。 また、このお作法で書いておくと、wireがシュッと導入できます。規模が大きくなってDIを手で書きたくなくなりそうな見込みがあれば、則っておくのが無難です。

func sample1() {
    ...
    r := NewTarget()
    r.Read(buf)

    r.Value // Unresolved reference 'Value'
    ...
}

また、関数の引数としてinterfaceを受け取るようにしておけば、Targetに依存しない汎用的な関数ができます。 ただ、Targetに依存しない代わりにio.Readerに依存していることに注意が必要です。 ioパッケージであれば問題ではないですが、それが自作のほかのパッケージである場合など、依存関係が不必要に複雑になっていないかよく考えるべきです。

func sample2(r io.Reader) {
    ...
    r.Read(buf)
    ...
}

このように、interfaceによって実装を隠蔽したり、依存先を実際の実装とは別のパッケージに向けることができます。 当然、むやみに使うのは混乱と手間を生みますが、適切な設計によってコントロールすることで、依存方向と各パッケージの責務の分割ができます。

モック

interfaceで扱っておくと、モックで実装を差し替えることができます。 たとえば、データソースに値を保存するRepositoryインタフェースを考えます。(さっそく"-er"の命名規則をぶっちぎっていますが・・)

type Repository interface {
    Get() (string, error)
    Save(val string) error
}

上位のモジュールのテストでは、Saveが成功するケースや、失敗のパターンごとのテストが書きたいわけですが、プロダクション用に書いたコードはそんなにいろんなバリエーションで失敗してくれるわけではありません。

type MockRepository struct{
    Repository
}

func (m MockRepository) Save(val string) error {
    return errors.New("not found")
}

簡単なものであれば、単純にダミー実装をしてしまえばよいです。テストに必要ない部分については、先に説明した埋め込みで対処してしまう方法があります。 より高度なものを考えるとすると、テストごとに処理を差し替え可能なものにしておく実装や、gomockなどのもモック用ライブラリの利用も検討の価値があります。

type MockRepository2 struct{
    MockGet func() (string, error)
    MockSave func(val string) error
}

func (m MockRepository2) Get() (string, error) {
    return m.MockGet()
}

func (m MockRepository2) Save(val string) error {
    return m.MockSave(val)
}

まとめ

とても基本的なinterfaceとはなんぞや?というところから、Goのinterfaceの使い方を紹介しました。 私のチームでは設計まわりで議論になることが多いので、整理も兼ねてまとめてみました。

We are hiring!

エムスリー、とくにBIRではGo/Reactエンジニアや、データ基盤開発に興味があるエンジニアを募集しています。社員とカジュアルにお話することもできますので、興味を持たれた方は下記よりお問い合わせください。

open.talentio.com

jobs.m3.com