BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

「Goらしさ」について考えてみる #1 interface編 “Accept interfaces, return structs” を添えて

はじめに

この記事は🎄🎅 BASE PRODUCT TEAM BLOG Advent Calendar 2025 🎅🎄の3日目の記事です。 devblog.thebase.in

こんにちは! BASE 株式会社 Pay ID 兼 BASE PRODUCT TEAM BLOG 編集局メンバー の @zan_sakurai です。

私の所属する Pay ID では一部のアプリケーションでGoを採用しており、日々Goらしいコードを書くことを意識して開発を行っています。
読者のみなさまは「Goらしさ」という言葉を聞いたことがある、もしくは使ったことはありますか...?
私も日々の開発シーンで聞いたことも使ったこともありますが、実際に「Goらしさとは何か?」と問われると、私も正直言葉に詰まってしまいます...。

とはいえ「Goらしさ」とは曖昧なままではあるのもよろしくないので、
本記事では一旦「Goらしさ」/「Goらしいコード」とは、Goの言語仕様だけでなく、Goの設計思想や慣習に沿ったコードを指すこととして、
Go の特徴の一つである interface を題材に、今回は「Goらしい」 interface の書き方について掘り下げてみたいと思います。

「Goらしい」 interface を書く

Go の interface の特徴は多岐にわたりますが、
今回はGo Wiki: Go Code Review Comments#Interfaces に記載されている内容を掘り下げていこうと思います。
具体的なコード例はGo Code Review Commentsがとても良くまとまっているので、そちらもぜひ御覧ください。

ステップ1: interface が本当に必要になるまで書かないのが「Goらしい」

Go Wiki: Go Code Review Comments の Interfaces の章には以下のように記されています。

Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain.

引用元: Go Wiki: Go Code Review Comments#Interfaces

端的にまとめてしまうと必要性が出てきたらinterfaceを定義せよ、という旨です。
私のようなJavaなどのOOP言語からGoに来た人は、最初に抽象型を定義してから具体的な実装を作る傾向があるかもしれませんが、
Goでは具体的な実装を行ってから必要性が出てきた時に初めてinterfaceを定義するのが「Goらしい」書き方のようです。
過度な抽象化による複雑化、いわゆる「インターフェース汚染」などと巷では呼ばれていますが、このようなことを避けるために、interfaceを書くタイミングに慎重になることが推奨されているようです。

Go Code Review Comments の例を参考に、ステップ1を踏まえたコードを書いてみました。

package consumer

import "interfaceidioms/step1/producer"

// 何らかの処理を行う構造体
// producerパッケージのThingerを内部に持つ
type ThingerConsumer struct {
    t producer.Thinger
}

// 何らかの処理を行うメソッド
func (tc ThingerConsumer) DoSomething(input producer.Input) bool {
    return tc.t.Thing(input)
}

// いわゆるファクトリ関数
// consumer.NewThingerConsumer(producer.NewThinger()) のような使われ方が想定される.
func NewThingerConsumer(t producer.Thinger) ThingerConsumer {
    return ThingerConsumer{t: t}
}
package producer

// 何らかの入力データを表す構造体
type Input struct {
    Content string
}

// 何らかの処理を行う構造体
type Thinger struct {
    // snip...
}

// 何らかの処理を行うメソッド
func (t Thinger) Thing(input Input) bool {
    return input.Content == ""
}

// いわゆるファクトリ関数
func NewThinger() Thinger {
    return Thinger{}
}

特に何の変哲もない?コードですが、ステップ1を踏まえたコードになっています。
「Goらしい」interfaceを書く上での最初のステップは、本当にinterfaceが必要になるまで書かないことです。

ステップ2: 消費する側に interface を定義すると 「Goらしい」

ステップ1で本当にinterfaceが必要になるまで書かないことを述べましたが、実際にinterfaceを書く必要性が出てきた場合、どう書くと「Goらしい」のでしょうか?
またまた Go Wiki: Go Code Review Comments の Interfaces の章からの引用ですが、以下のように記されています。

Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. The implementing package should return concrete (usually pointer or struct) types: that way, new methods can be added to implementations without requiring extensive refactoring.

引用元: Go Wiki: Go Code Review Comments#Interfaces

これも端的にまとめてしまうと、interfaceは実装を提供する側ではなく、消費する側のpackageに定義すべき、という旨が記載されています。
この点も私のようなJavaなどのOOP言語からGoに来た人は、実装を提供する側でinterfaceを宣言したくなるかもしれませんが、
Goでは消費する側でinterfaceを定義するのが「Goらしい」書き方のようです。

Go では明示的にimplementsを宣言せず、Duck Typing的に暗黙的にinterfaceを満たすので、
ステップ1で直接呼びだしている箇所も後からinterfaceに置き換えるリファクタリングは容易です。
また、消費する側が使うメソッドだけをinterfaceに含めば良いので、小さなinterfaceを定義できます。(いわゆるインターフェース分離の原則。)
小さなinterfaceですので、テスト時に必要な振る舞いだけを持つinterfaceを満たすモック実装を用意するのも容易です。

Go Code Review Comments の例を参考に、ステップ2を踏まえたコードを書いてみました。

package step2goodconsumer

import "interfaceidioms/step1/producer" // step1で使ったproducerパッケージの変更はなく、そのままstep2goodconsumer側でinterfaceを定義できる.

// Thinger interface を定義
// これがいわゆる消費者側のinterface.
type Thinger interface {
    Thing(input producer.Input) bool
}

// 何らかの処理を行う構造体
// producerパッケージのThingerを内部に持つ
type ThingerConsumer struct {
    // interface型を使うように変更
    t Thinger
}

// 何らかの処理を行うメソッド
func (tc ThingerConsumer) DoSomething(input producer.Input) bool {
    return tc.t.Thing(input)
}

// いわゆるファクトリ関数
// DIなりでよしなに入れ替えよう.
func NewThingerConsumer(t producer.Thinger) ThingerConsumer {
    return ThingerConsumer{t: t}
}

実際のstep1との差分も少なく、step2goodconsumer側の判断でproducerの変更なく、容易に interface を使った書き方にリファクタリングできることがわかります。

$ diff -u --label "step1/consumer.go" --label "step2/consumer.go" \\
    step1/consumer/consumer.go step2goodconsumer/consumer.go
--- step1/consumer.go
+++ step2/consumer.go
@@ -1,11 +1,18 @@
-package consumer
+package step2goodconsumer

-import "interfaceidioms/step1/producer"
+import "interfaceidioms/step1/producer" // step1で使ったproducerパッケージの変更はなく、そのままstep2goodconsumer側でinterfaceを定義できる.
+
+// Thinger interface を定義
+// これがいわゆる消費者側のinterface.
+type Thinger interface {
+       Thing(input producer.Input) bool
+}

 // 何らかの処理を行う構造体
 // producerパッケージのThingerを内部に持つ
 type ThingerConsumer struct {
-       t producer.Thinger
+       // interface型を使うように変更
+       t Thinger
 }

 // 何らかの処理を行うメソッド
@@ -14,7 +21,7 @@
 }

 // いわゆるファクトリ関数
-// consumer.NewThingerConsumer(producer.NewThinger()) のような使われ方が想定される.
+// DIなりでよしなに入れ替えよう.
 func NewThingerConsumer(t producer.Thinger) ThingerConsumer {
        return ThingerConsumer{t: t}
 }

「Goらしい」interfaceを書く上での次のステップは、消費する側に interface を定義するです。

番外編: Accept interfaces, return structs / Accept Interfaces, Return Concrete Types

Accept interfaces, return structs という言葉を聞いたことはありますか? いわゆる interfaceを受け入れ、具体的な型を返す、というidiomです。
この際に改めて原典を探ってみましたが、詳しく言及しているようなものはあまりないように見受けられました。
もし原典をご存知の方がいらっしゃいましたら、ぜひ教えてください。(以下触れている箇所)

ステップ1とステップ2を踏まえた上で、ですとproducer側でinterfaceを定義するケースはまだないので、自ずとAccept interfaces, return structsになるのかと思います。

余談

今回はあえて、producer側で定義するinterfaceについては触れませんでした。
実際に https://github.com/golang/go で標準ライブラリのコードを見てみると、producer側でinterfaceを提供しているケースも多々あります。
続編を書こうと思いますので、ご期待いただけますと幸いです。

さいごに

最後に改めて「Goらしい」 interface を書くステップをまとめます。

  1. 本当にinterfaceが必要になるまで書かないこと
  2. 消費する側に interface を定義する

読者のみなさまが「Goらしさ」に触れるきっかけの一助になれば幸いです。

参考資料

宣伝

Pay ID ではエンジニアを募集中です!ご興味があれば採用情報もぜひご覧ください!

open.talentio.com

明日は、BASEアドベントカレンダーは @tanden さんの記事です。お楽しみに!