go-interface-design について
Go の開発者やコーディングエージェント向けのインターフェース設計スキルです。定義場所や規模の指針、よくある設計上の落とし穴を扱います。
主な機能
- 01利用側でインターフェースを定義
- 02小さく合成可能な設計
- 03インターフェースを受け具体型を返す
- 04コンパイル時の実装検証
- 05インターフェースへのポインターを回避
- 06埋め込みと型アサーションの注意点
活用シーン
- 01利用側の要件に沿ったパッケージ境界の設計
- 02広すぎないテスト用の接点の追加
- 03Go コードのインターフェース反パターンの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-interface-designインストール後、Claude Code で/go-interface-designを入力して読み込まれたか確認します。
同梱ファイルreferences/embedding-and-assertions.md
詳細
Go インターフェースの設計
Go のインターフェースは暗黙的です。これはこの言語で最も重要な設計上の特徴であり、Java や C# から移ってきた人の多くは、最初はこの点を正しく理解できません。
コミュニティのデフォルトです。
go-interface-designより明示的に優先されるプロジェクトのスキルまたはエージェント設定ルールがある場合は、そちらが優先されます。
詳細なリファレンス資料は必要に応じて読み込んでください。
references/embedding-and-assertions.md— 埋め込みと名前付きフィールドの比較、インターフェース埋め込みを使ったテスト用フェイク、オプションの機能、シールドインターフェース。
以下の概要だけでは不十分な場合にのみ、リファレンスファイルを読んでください。
1. 大原則:インターフェースは利用側で定義する
振る舞いの利用側がインターフェースを定義します。提供側ではありません。
// ❌ Wrong — producer defines interface (Java thinking)
// package store
type UserStore interface { // defined alongside implementation
GetByID(ctx context.Context, id string) (*User, error)
Create(ctx context.Context, user *User) error
// ... 15 more methods
}
type PostgresStore struct { ... }
func (s *PostgresStore) GetByID(...) { ... }
func (s *PostgresStore) Create(...) { ... }
// ✅ Right — consumer defines what it needs
// package service
type UserReader interface { // only what THIS service needs
GetByID(ctx context.Context, id string) (*domain.User, error)
}
type UserService struct {
store UserReader // depends on narrow interface
}
// package store (no interface defined here)
type PostgresStore struct { db *sql.DB }
func (s *PostgresStore) GetByID(ctx context.Context, id string) (*domain.User, error) { ... }
func (s *PostgresStore) Create(ctx context.Context, user *domain.User) error { ... }
// PostgresStore satisfies service.UserReader implicitly — no declaration needed
これが重要な理由:
- 利用側が依存するのは、使用するものだけです(インターフェース分離の原則)。
- 提供側はメソッドを追加しても、利用側を壊しません。
- テストに必要なのは、利用側が呼び出すメソッドだけです。
- インポートサイクルが発生しません。利用側は提供側のパッケージをインポートしないためです。
2. インターフェースは小さく保つ
インターフェースが大きいほど、抽象化は弱くなります。
// ✅ Good — focused, composable
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
// ❌ Bad — kitchen sink interface
type FileManager interface {
Read(path string) ([]byte, error)
Write(path string, data []byte) error
Delete(path string) error
List(dir string) ([]string, error)
Move(src, dst string) error
Copy(src, dst string) error
Stat(path string) (os.FileInfo, error)
Watch(path string) (<-chan Event, error)
}
指針:1~3 個のメソッドが理想です。それ以上必要な場合は、小さなインターフェースを組み合わせてください。
3. インターフェースを受け取り、構造体を返す
// ✅ Good — accepts interface, returns concrete type
func NewUserService(store UserReader, logger Logger) *UserService {
return &UserService{store: store, logger: logger}
}
// ❌ Bad — returns interface (hides the concrete type for no reason)
func NewUserService(store UserReader) UserServiceInterface {
return &UserService{store: store}
}
呼び出し側が型のすべてのメソッドを使えるように、具体型を返してください。インターフェースを返すのが適切なのは、入力に応じて関数が異なる具体型を実際に返す場合(ファクトリーパターン)だけです。
4. コンパイル時にインターフェースへの適合を確認する
空識別子への代入を使い、契約の破損を早期に検出します。
// Verify *PostgresStore implements service.UserReader at compile time
var _ service.UserReader = (*PostgresStore)(nil)
// Verify LogHandler implements http.Handler
var _ http.Handler = (*LogHandler)(nil)
// For value receivers:
var _ fmt.Stringer = Status(0)
これらは型宣言の直後に配置してください。実行時のコストはなく、契約の破損を気付かないままにするのを防げます。
5. インターフェースへのポインターは使わない
// ❌ Bad — pointer to interface is almost never correct
func process(r *io.Reader) { ... }
// ✅ Good — interface is already a pointer internally
func process(r io.Reader) { ... }
インターフェース値は内部的に 2 つのポインター(型とデータ)で構成されています。インターフェースへのポインターは、ポインターへのポインターであり、不要な間接参照です。
唯一の例外は、インターフェース値そのものを置き換える必要がある場合(実行時に実装を入れ替える場合)ですが、これは極めてまれです。
6. 空インターフェース
interface{}(または Go 1.18 以降の any)は、型安全性を放棄することを意味します。使用は控えめにしてください。
// ✅ Acceptable — generic container before generics / stdlib compatibility
func Marshal(v any) ([]byte, error)
// ✅ Better (Go 1.18+) — use generics instead of any
func Map[T, U any](slice []T, fn func(T) U) []U { ... }
// ❌ Bad — lazy interface design
func Process(data any) any { ... } // what does this even do?
7. オプション設定はインターフェースの問題ではない
コンストラクターのオプション設定は、インターフェースを導入する理由にはなりません。関数型オプションまたは設定構造体を使ってください。両方の方法と、そのトレードオフについては go-design-patterns スキルで扱います。
8. よくあるインターフェースのアンチパターン
時期尚早なインターフェース:
// ❌ Bad — interface defined before second implementation exists
type Processor interface {
Process(ctx context.Context, data []byte) error
}
type processor struct { ... } // only one implementation ever
// ✅ Good — use concrete type until you need the abstraction
type Processor struct { ... }
// Add interface when you have 2+ implementations or need testing seam
「インターフェースを使って設計するのではなく、必要になったときに見つけ出す。」— Rob Pike
インターフェースの乱用:
// ❌ Bad — wrapping every struct in an interface "for testability"
type UserServiceInterface interface { ... }
type OrderServiceInterface interface { ... }
type PaymentServiceInterface interface { ... }
// 50 more interfaces with exactly one implementation each
// ✅ Good — define interfaces where they're consumed
// Each consumer declares only the methods IT needs
シールドインターフェースと列挙型:
シールドインターフェース(非公開のマーカーメソッドを使い、このパッケージだけがバリアントを追加できるようにする方法)は、異なるデータを持ち、型スイッチで処理するバリアントの集合を限定したい場合に適しています。バリアントがデータを持たない場合は、代わりに型付き定数の列挙型を使ってください。例:
references/embedding-and-assertions.md。
9. 埋め込みと型アサーション
- 埋め込みはメソッドセット全体を昇格させます。エクスポートされた構造体に
sync.Mutexを埋め込むと、Lock/Unlockが公開されます。外側の型が内側の型を拡張したものとして本当に同一である場合を除き、名前付きフィールドを使ってください。 - 構造体に埋め込まれたインターフェースは、呼び出されるとパニックするメソッドによって実装を満たします。テスト用フェイクでは問題ありませんが、それ以外では隠れた実行時障害になります。
- オプションの機能には、インターフェースへの型アサーション(
w.(http.Flusher))を使い、必ずカンマ ok 形式にしてください。 - ジェネリックメソッド(Go 1.27 以降)はインターフェースのメソッドを満たしません。インターフェースとして定義するもののメソッドは、非ジェネリックにしてください。
判断チェックリスト
- ここにインターフェースは必要ですか? — 実装が2つ以上ある場合、テスト用の差し替え口が必要な場合、またはパッケージの境界をまたぐ場合にのみ必要です。
- どこで定義すべきですか? — 提供側ではなく、利用側です。
- メソッドはいくつ必要ですか? — 少ないほどよいです。1~3個が理想です。
- インターフェースを返していますか? — おそらく避けるべきです。具象型を返しましょう。
- 準拠を確認しましたか? —
var _ Interface = (*Type)(nil) - 利便性のために埋め込みを使っていますか? — 昇格されたすべてのメソッドが外側の型に属する場合を除き、名前付きフィールドを使いましょう。