📚 背景知識(読んでから問題へ)
Day 019では「型を増やすコスト」を意識してエラー型を設計しました。今日はその視点をインターフェース設計に広げます。テーマはGoコミュニティで最も有名な格言のひとつ、"Accept interfaces, return structs"(インターフェースを受け取り、構造体を返せ)です。
Goの標準ライブラリを見渡すと、インターフェースの多くが驚くほど小さいことに気づきます。
io.Reader:Read(p []byte) (n int, err error)の1メソッドのみio.Writer:Write(p []byte) (n int, err error)の1メソッドのみsort.Interface:Len()Less(i, j int) boolSwap(i, j int)の3メソッドのみerror:Error() stringの1メソッドのみ
これは偶然ではなく、Goの設計思想の核心です。理由は3つあります。
- 実装しやすい: メソッドが少ないほど、そのインターフェースを満たす型を作るコストが下がる。1メソッドのインターフェースなら、既存のどんな型にも簡単にアダプタを書ける
- テストしやすい: 関数が必要とするインターフェースが小さいほど、テスト用のフェイク実装が数行で書ける。巨大なインターフェースは「使わないメソッドまでダミー実装する」羽目になる
- 依存が明確になる: 関数のシグネチャに小さいインターフェースが並ぶと、「この関数は本当は何が必要なのか」が一目でわかる。巨大な万能インターフェースを受け取る関数は、実際に何を使っているのか読まないとわからない
もう1つ重要なのが「インターフェースは実装側ではなく利用側(consumer)が定義する」という原則です。Javaなどでは「このクラスが実装するインターフェース」を実装側のパッケージで先に定義することが多いですが、Goでは逆で、「この関数が必要とする最小限の振る舞い」を呼び出し側のパッケージで定義するのが自然です。これにより、同じ具象型(例: DBクライアント)に対して、呼び出し側ごとに異なる粒度のインターフェースを自由に定義できます。
そして"return structs"の部分は、コンストラクタ(NewXxx)は具体的な構造体のポインタを返すべきで、インターフェースを返すべきではない、という指針です。呼び出し元がその構造体の全メソッドにアクセスできる状態を保ち、必要なメソッドだけを使いたい別の呼び出し元は、自分でその構造体を受け取れる小さいインターフェースを定義すればよい、という役割分担です。
📝 問題
あなたのチームには、以下のような「万能インターフェース」があります。データベースのキーバリューストアを表すDataStoreインターフェースで、複数の呼び出し元(レポート生成・バッチ削除処理・管理画面)がこれを直接受け取って使っています。
type DataStore interface {
Get(ctx context.Context, key string) (string, bool, error)
Put(ctx context.Context, key, value string) error
Delete(ctx context.Context, key string) error
List(ctx context.Context, prefix string) ([]string, error)
Close() error
}
このうち「レポート生成処理」は、指定されたキー一覧の値を取得して出力するだけの機能です。しかし現状のコードはfunc GenerateReport(store DataStore, keys []string, w io.Writer) errorのように、5メソッドすべてを持つDataStoreをまるごと受け取っています。
あなたのタスク
GenerateReportが本当に必要としているメソッドを見極め、利用側(report生成処理)で定義する最小限のインターフェースを設計することGenerateReportのシグネチャをその小さいインターフェースを受け取る形に変更すること- 具体的なデータストア実装として
PostgresStore構造体を実装し、DataStoreの5メソッドすべてを実装すること。コンストラクタNewPostgresStore()は具体的な構造体のポインタ(インターフェースではない)を返すこと main関数でNewPostgresStore()の戻り値をそのままGenerateReportに渡し、Goのインターフェースが暗黙的に満たされることを実演すること(PostgresStoreが小さいインターフェースを「実装しています」と明示的に書く必要がないことを確認する)- テスト用の
fakeStore型を実装すること。このときDataStoreの5メソッドをすべてダミー実装するのではなく、小さいインターフェースが要求する1メソッドだけを実装すればテストが書けることを、実際にgo testが通るテストコードで示すこと - 設計判断として、なぜこの粒度でインターフェースを切ったのか、「利用側が定義する」という原則をどう適用したかを1〜2文で説明すること
🔍 ヒント(段階的開示)
ヒント1 — 方向性
GenerateReportの処理内容(キーを渡して値を取得し、io.Writerに書き出すだけ)を1行ずつ読み、実際にどのメソッドを呼んでいるかを数えてみてください。Put・Delete・List・Closeを呼んでいる箇所は1つもないはずです。「将来使うかもしれないから」という理由でメソッドを含めるのはYAGNI違反です。
ヒント2 — アプローチ
- 小さいインターフェースの名前は「何をする型か」ではなく「何ができる型か」を表す
-er接尾辞が自然です(io.ReaderならReadできるもの、というように)。Getだけが必要ならDataGetterのような名前が自然です - このインターフェースは
DataStoreを定義しているパッケージではなく、GenerateReportを定義しているパッケージ(利用側)に置いてください。Goでは型がインターフェースを「実装します」と明示的に宣言する必要がないため、PostgresStore側はDataGetterの存在を一切知らなくても、メソッドのシグネチャが一致していれば自動的に満たしたことになります fakeStoreはmap[string]stringをラップした型にして、Getメソッドだけを生やせば十分です。Put/Delete/List/Closeは実装不要です(コンパイルエラーになりません)
ヒント3 — コード骨格
// report/report.go
package report
import (
"context"
"fmt"
"io"
)
// DataGetter は GenerateReport が実際に必要とする最小限の振る舞いのみを表す。
// 利用側(report パッケージ)がここで定義する。
type DataGetter interface {
// TODO: 必要なメソッド1つだけを定義する
}
func GenerateReport(ctx context.Context, store DataGetter, keys []string, w io.Writer) error {
// TODO: keys を順に Get し、見つかった/見つからなかったを w に書き出す
return nil
}
// store/postgres.go
package store
import "context"
// DataStore は複数の呼び出し元(report・batch削除・管理画面)が
// 共通で参照する「実装側」の万能インターフェース定義(今回は比較用に残す)。
type DataStore interface {
Get(ctx context.Context, key string) (string, bool, error)
Put(ctx context.Context, key, value string) error
Delete(ctx context.Context, key string) error
List(ctx context.Context, prefix string) ([]string, error)
Close() error
}
type PostgresStore struct {
// TODO: 内部状態(今回はデモ用にmapで代用してよい)
}
func NewPostgresStore() *PostgresStore {
// TODO: 具体的な構造体のポインタを返す(インターフェース型を返さない)
return nil
}
// TODO: PostgresStore に DataStore の5メソッドをすべて実装する
✅ 模範解答
ファイル: report/report.go
package report
import (
"context"
"fmt"
"io"
)
// ── 設計判断 ──────────────────────────────────────────
// GenerateReport が実際に呼び出しているのは Get の1メソッドのみ。
// Put/Delete/List/Close は一度も使われないため、report パッケージ
// (=利用側)で「本当に必要な最小限」だけを持つ DataGetter を定義する。
// このインターフェースは store パッケージの DataStore とは無関係に、
// report パッケージが「自分にとって何が必要か」だけを基準に定義したもの。
// PostgresStore 側は DataGetter の存在を知らなくても、Get のシグネチャが
// 一致していれば自動的にこのインターフェースを満たす(暗黙的実装)。
// ─────────────────────────────────────────────────────
// DataGetter は GenerateReport が必要とする最小限の振る舞い。
// 利用側(report パッケージ)が定義する、いわゆる consumer-defined interface。
type DataGetter interface {
Get(ctx context.Context, key string) (value string, found bool, err error)
}
// GenerateReport は keys のそれぞれについて値を取得し、w に書き出す。
// store には DataGetter という「Getできるものなら何でもよい」という
// 最小限の要求だけを課しているため、PostgresStore・フェイク・将来の
// Redisストアなど、Getさえ実装していればどんな型でも渡せる。
func GenerateReport(ctx context.Context, store DataGetter, keys []string, w io.Writer) error {
for _, key := range keys {
value, found, err := store.Get(ctx, key)
if err != nil {
return fmt.Errorf("generate report: get %q: %w", key, err)
}
if !found {
fmt.Fprintf(w, "%s: (not found)\n", key)
continue
}
fmt.Fprintf(w, "%s: %s\n", key, value)
}
return nil
}
ファイル: store/postgres.go
package store
import (
"context"
"fmt"
)
// DataStore は複数の呼び出し元(report・batch削除・管理画面)が
// 実際のDB操作をイメージしやすいよう、実装側でひとまとめに定義した
// リファレンス用インターフェース。これ自体は「万能インターフェース」の
// 例として残しているが、各呼び出し元が直接これを受け取ることは推奨しない。
type DataStore interface {
Get(ctx context.Context, key string) (string, bool, error)
Put(ctx context.Context, key, value string) error
Delete(ctx context.Context, key string) error
List(ctx context.Context, prefix string) ([]string, error)
Close() error
}
// PostgresStore は DataStore の実運用実装(デモのため内部はmapで代用)。
type PostgresStore struct {
data map[string]string
}
// NewPostgresStore は具体的な構造体のポインタを返す。
// インターフェース型(DataStore)を返さないことで、呼び出し元は
// PostgresStore が実際に持つ全メソッドにアクセスでき、必要な範囲だけを
// 使いたい呼び出し元は自分で小さいインターフェースを定義して受け取ればよい。
func NewPostgresStore() *PostgresStore {
return &PostgresStore{data: map[string]string{
"user:1:name": "Alice",
"user:2:name": "Bob",
"config:limit": "1000",
}}
}
func (s *PostgresStore) Get(ctx context.Context, key string) (string, bool, error) {
v, ok := s.data[key]
return v, ok, nil
}
func (s *PostgresStore) Put(ctx context.Context, key, value string) error {
s.data[key] = value
return nil
}
func (s *PostgresStore) Delete(ctx context.Context, key string) error {
delete(s.data, key)
return nil
}
func (s *PostgresStore) List(ctx context.Context, prefix string) ([]string, error) {
var keys []string
for k := range s.data {
if len(k) >= len(prefix) && k[:len(prefix)] == prefix {
keys = append(keys, k)
}
}
return keys, nil
}
func (s *PostgresStore) Close() error {
fmt.Println("postgres connection closed")
return nil
}
ファイル: main.go(同一モジュール内でexample.com/report・example.com/storeとしてimport)
package main
import (
"context"
"os"
"example.com/report"
"example.com/store"
)
func main() {
ctx := context.Background()
// NewPostgresStore() は *PostgresStore という具体的な構造体を返す。
// PostgresStore は report.DataGetter の存在を知らないが、
// Get(ctx, key) (string, bool, error) というシグネチャを持つため
// 暗黙的に report.DataGetter を満たしている。
pg := store.NewPostgresStore()
keys := []string{"user:1:name", "user:2:name", "config:limit", "config:missing"}
if err := report.GenerateReport(ctx, pg, keys, os.Stdout); err != nil {
panic(err)
}
_ = pg.Close()
}
ファイル: report/report_test.go
package report
import (
"bytes"
"context"
"strings"
"testing"
)
// fakeStore は report.DataGetter が要求する Get メソッドだけを実装する。
// store.DataStore の Put/Delete/List/Close は一切実装していないが、
// GenerateReport が要求するインターフェースはこれで満たしているため
// コンパイル・テストともに問題なく通る。
type fakeStore struct {
data map[string]string
}
func (f *fakeStore) Get(ctx context.Context, key string) (string, bool, error) {
v, ok := f.data[key]
return v, ok, nil
}
func TestGenerateReport(t *testing.T) {
fs := &fakeStore{data: map[string]string{"a": "1", "b": "2"}}
var buf bytes.Buffer
err := GenerateReport(context.Background(), fs, []string{"a", "b", "c"}, &buf)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := buf.String()
wantLines := []string{"a: 1", "b: 2", "c: (not found)"}
for _, line := range wantLines {
if !strings.Contains(got, line) {
t.Errorf("output missing line %q, got:\n%s", line, got)
}
}
}
▶ 実行結果を見る(go run . / go test ./... で検証済み)
$ go run .
user:1:name: Alice
user:2:name: Bob
config:limit: 1000
config:missing: (not found)
postgres connection closed
$ go test ./...
ok example.com/report 0.002s
※ report・storeパッケージとmain.goを1つのモジュール(go.modでmodule example.com/xxxと宣言し、report/report.go・store/postgres.goを配置)として実行しています。
🪜 Step-by-Step 解説
GenerateReportのロジックを読むと、呼んでいるのはstore.Get(...)だけです。Put/Delete/List/Closeは登場しません。この「実際に使っている操作を数える」という地味な作業が、インターフェース設計の出発点になります。読まずに「DBを触るからDataStoreをそのまま渡そう」とするのが、巨大インターフェースが量産される典型的な原因です。
type DataGetter interface {
Get(ctx context.Context, key string) (value string, found bool, err error)
}
このインターフェースはstoreパッケージではなくreportパッケージに置きます。「reportが何を必要としているか」という文脈でしか意味を持たない定義だからです。もし将来batchパッケージがDeleteだけを必要とするなら、batchパッケージ側でDataDeleterという別の1メソッドインターフェースを独自に定義すればよく、reportの定義とは無関係に育てられます。
func NewPostgresStore() *PostgresStore {
return &PostgresStore{...}
}
func NewPostgresStore() DataStoreのようにインターフェースを返す設計にしなかった点がポイントです。インターフェースを返すと、呼び出し元はDataStoreが持つメソッドしか使えなくなり、PostgresStoreが将来Ping()のような固有メソッドを追加しても呼び出し元から使えません。具体的な構造体を返しておけば、呼び出し元は必要に応じて自分で小さいインターフェースに絞り込み(narrowing)できます。
main.goではpg := store.NewPostgresStore()の戻り値(*store.PostgresStore)を、そのままreport.GenerateReport(ctx, pg, ...)に渡しています。PostgresStore側のコードにはreport.DataGetterという文字列は一度も登場しません。Goのインターフェースは構造的部分型(structural typing)なので、メソッドシグネチャが一致していれば自動的に満たされます。この「知らないインターフェースを勝手に満たせる」性質が、利用側でインターフェースを定義するスタイルを可能にしています。
type fakeStore struct{ data map[string]string }
func (f *fakeStore) Get(ctx context.Context, key string) (string, bool, error) { ... }
fakeStoreが実装しているのはGetの1メソッドだけです。もしGenerateReportが巨大なDataStoreを受け取るシグネチャのままだったら、テストのためだけにPut/Delete/List/Closeのダミー実装(何もしない、またはpanic("not implemented"))を書く必要がありました。小さいインターフェースはテストコードの量を直接減らします。
💡 設計思想・なぜこう書くのか
Animal、Shapeなど)として設計されます。Goでは発想が逆で、インターフェースは「この関数が動くために最低限必要な契約」として設計されます。だから同じPostgresStoreという具象型に対して、reportパッケージはDataGetter(Getだけ)、batchパッケージはDataDeleter(Deleteだけ)というように、利用文脈ごとに異なる粒度のインターフェースが複数存在してよい、というのがGoらしい発想です。🌐 他言語との比較
| 観点 | Go | Java | Python | TypeScript |
|---|---|---|---|---|
| インターフェースの定義場所 | 利用側(呼び出し元)が「自分に必要な最小限」を定義するのが定石 | 実装側 or 共有のAPIパッケージで先に定義することが多い | 明示的なインターフェースはないが、Protocol(typing)は利用側で定義できる | 利用側でも実装側でも定義できるが、大きめのinterfaceを共有APIとして先に置く文化が強い |
| 実装の宣言 | 暗黙的(メソッドシグネチャが一致すれば自動的に満たす) | 明示的(implementsキーワードが必須) | 明示的継承 or Protocolなら暗黙的(構造的部分型) | 明示的(implements)だが、構造的型システムのため実質的には暗黙も可能 |
| 小さいインターフェースの文化 | 1〜3メソッドが一般的(io.Readerなど) | 比較的大きめのインターフェース(List, Collectionなど)が標準 | ダックタイピングが基本のため、インターフェース自体を明示しないことも多い | 構造的型システムのため小さい型を都度その場で書きやすいが、慣習として大きめの共有型を使うことも多い |
| コンストラクタの戻り値 | 具体的な構造体のポインタを返すのが慣習(return structs) | ファクトリメソッドがインターフェース型を返すことが一般的(DIコンテナとの相性重視) | 型ヒントとしてクラス自体を返すのが一般的 | ファクトリ関数がインターフェース型を返すことも構造体的な型を返すことも両方一般的 |
Goで際立つのは、「暗黙的実装」と「利用側でのインターフェース定義」がセットになっている点です。実装側がインターフェースの存在を知らなくてよいため、後から新しい利用文脈(新しい小さいインターフェース)を追加しても、既存の実装コードに一切手を入れる必要がありません。これはJava的な「先にインターフェースを設計してから実装する」ワークフローとは順序が逆で、「まず具体的な実装を書き、必要になった時点で利用側からインターフェースを切り出す」というボトムアップの設計がGoでは自然です。
🏆 実務での使いどころ
- テスト容易性の確保: DBクライアント・外部APIクライアント・ファイルシステムアクセスなど、外部依存を持つ関数を書くときは、その関数が本当に必要とする操作だけを持つ小さいインターフェースを利用側で定義しておくと、テストで本物のDB/APIを使わずにフェイクへ差し替えられる
- 段階的なマイクロサービス分割:
reportパッケージがDataGetterしか要求していなければ、将来PostgresをRedisやリモートAPI経由のストアに置き換えるときも、reportパッケージのコードは一切変更不要(Getさえ実装していれば何でも渡せる) - 依存関係の可視化: 関数シグネチャに並ぶ小さいインターフェース群を見るだけで、コードレビュー時に「この関数は何に依存しているか」が一目でわかる。巨大インターフェースだとレビュアーが実装を読まないと依存範囲を把握できない
- 標準ライブラリとの親和性:
io.Reader/io.Writerのような標準の小さいインターフェースに準拠して自作の型を設計しておくと、io.Copyやbufioなど標準ライブラリの関数群にそのまま接続できる
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 実装側のパッケージに「まとめて使えるように」と大きなインターフェースを先に定義してしまう | Java的な「インターフェースファースト」の設計習慣が抜けない | Goでは利用側が必要な分だけを定義するのが自然。実装側は具象型を素直に公開すればよく、インターフェースを事前に用意する必要はない |
コンストラクタの戻り値をインターフェース型にしてしまう(func New() DataStore) | 「実装を隠蔽した方が安全」という直感、あるいはDIパターンの模倣 | 呼び出し元が本来アクセスできたはずの固有メソッド(Closeのような型固有の操作)が使えなくなる。具体的な構造体を返し、絞り込みは呼び出し元に任せる |
| テストのために巨大インターフェースの全メソッドをダミー実装してしまう | 既存の巨大インターフェースをそのまま使い回そうとする | 関数のシグネチャ自体を小さいインターフェースに変更すれば、テスト側の実装コストも自然に小さくなる。テストが辛いのは設計側の問題であることが多い |
| 「将来使うかもしれない」メソッドをインターフェースに含めてしまう | 拡張性への漠然とした不安 | 実際に必要になった時点でインターフェースにメソッドを追加すればよい。Goの暗黙的実装なら、既存の実装型に手を加えなくてもメソッドを増やせるケースが多く、先回りする必要性は薄い |
🚀 次のステップ
- 発展:
batchパッケージ(キーの一括削除処理)を想定し、DataDeleterという別の1メソッドインターフェースを定義して、同じ*store.PostgresStoreをreport.DataGetterとbatch.DataDeleterの両方に渡せることを確認してみましょう。1つの具象型が複数の異なる利用側インターフェースを同時に満たせることを体感できます - 次回予告: Day 021 — embedding(構造体・インターフェースの埋め込み)(実装)。継承ではなくコンポジションでコードを再利用するGoの仕組みを、今日の「小さいインターフェース」の考え方の延長として学びます