golang-structs-interfaces について
開発者やコーディングエージェント向けの Go 型設計スキルです。構造体、インターフェース、ジェネリクス、コンポジションを扱い、利用側で定義する小さなインターフェースと具体型の返却を重視します。
主な機能
- 01利用側で小さなインターフェースを定義
- 02具体型を返す設計を優先
- 03構造体のゼロ値を有用に設計
- 04型安全な処理にジェネリクスを使用
- 05インターフェース検査と依存性注入を活用
活用シーン
- 01利用側パッケージのインターフェースを設計
- 02依存性注入でテスト可能なサービスを構築
- 03構造体とレシーバーの設計をレビュー
- 04コンパイル時にインターフェース実装を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ samber/ cc-skills-golang --skill golang-structs-interfacesインストール後、Claude Code で/golang-structs-interfacesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/struct-fields.mdtype-assertions.md詳細
ペルソナ: あなたは Go の型システム設計者です。小さく組み合わせ可能なインターフェースと、具体的な戻り値の型を重視します。抽象化そのものではなく、テストしやすさと明確さを重視して設計します。
コミュニティのデフォルト。
samber/cc-skills-golang@golang-structs-interfacesスキルを明示的に置き換える企業向けスキルがある場合は、そちらが優先されます。
Go の構造体とインターフェース
インターフェース設計の原則
インターフェースは小さく保つ
「インターフェースが大きいほど、抽象化は弱くなる。」— Go の格言
インターフェースはメソッドを1~3個にするべきです。小さなインターフェースは実装、モック、組み合わせが容易です。より大きな契約が必要な場合は、小さなインターフェースを組み合わせてください。
→ インターフェースの命名規則(メソッド名+「-er」接尾辞、標準的な名前)については、samber/cc-skills-golang@golang-naming スキルを参照してください。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Composed from small interfaces
type ReadWriter interface {
Reader
Writer
}
小さなインターフェースを組み合わせて、より大きなインターフェースを作成します。
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}
インターフェースは利用側で定義する
インターフェースは利用側に属します。
インターフェースは実装側ではなく、利用側で定義しなければなりません。これにより利用側が契約を管理でき、インターフェースだけを利用するためにパッケージをインポートする必要もなくなります。
// package notification — defines only what it needs
type Sender interface {
Send(to, body string) error
}
type Service struct {
sender Sender
}
email パッケージは具体的な Client 構造体をエクスポートします。Sender を知る必要はありません。
インターフェースを受け取り、構造体を返す
関数は柔軟性のためにインターフェース型の引数を受け取り、明確さのために具体的な型を返すべきです。呼び出し側は返された型のフィールドやメソッドをすべて利用できます。また、上流の利用側では必要に応じて、戻り値をインターフェース型の変数に代入できます。
// Good — accepts interface, returns concrete
func NewService(store UserStore) *Service { ... }
// Bad — an interface return hides every other method of the concrete type from callers
func NewService(store UserStore) ServiceInterface { ... }
インターフェースを早まって作成しない
「インターフェースを使って設計するのではなく、必要になったときに見いだしましょう。」
2つ目の実装が存在する前に書かれたインターフェースは、どのメソッドが変わるのかを推測したものです。その推測はたいてい外れるため、結局は抽象化を作り直すことになります。その間、間接層によって具体的な型が読者やツールから見えにくくなります。まずは具体的な型から始め、2つ目の利用側、2つ目の実装、またはテスト用モックが必要になった時点でインターフェースを抽出してください。
// Bad — premature interface with a single implementation
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type userRepository struct { db *sql.DB }
// Good — start concrete, extract an interface later when needed
type UserRepository struct { db *sql.DB }
ゼロ値を有用にする
明示的な初期化なしでも機能するように構造体を設計します。適切に設計されたゼロ値は、コンストラクターの定型コードを減らし、nil に関連するバグを防ぎます。
// Good — zero value is ready to use
var buf bytes.Buffer
buf.WriteString("hello")
var mu sync.Mutex
mu.Lock()
// Bad — zero value is broken, requires constructor
type Registry struct {
items map[string]Item // nil map, panics on write
}
// Good — lazy initialization guards the zero value
func (r *Registry) Register(name string, item Item) {
if r.items == nil {
r.items = make(map[string]Item)
}
r.items[name] = item
}
具体的な型で足りる場合は any / interface{} を避ける
Go 1.18 以降では、型安全な操作には any よりジェネリクスを優先しなければなりません。型が本当に不明な境界(JSON のデコードやリフレクションなど)でのみ any を使用してください。
// Bad — loses type safety
func Contains(slice []any, target any) bool { ... }
// Good — generic, type-safe
func Contains[T comparable](slice []T, target T) bool { ... }
標準ライブラリの主要なインターフェース
| インターフェース | パッケージ | メソッド |
|---|---|---|
Reader | io | Read(p []byte) (n int, err error) |
Writer | io | Write(p []byte) (n int, err error) |
Closer | io | Close() error |
Stringer | fmt | String() string |
error | builtin | Error() string |
Handler | net/http | ServeHTTP(ResponseWriter, *Request) |
Marshaler | encoding/json | MarshalJSON() ([]byte, error) |
Unmarshaler | encoding/json | UnmarshalJSON([]byte) error |
標準的なメソッドシグネチャを必ず守らなければなりません。型に String() メソッドがある場合は、fmt.Stringer と一致している必要があります。ToString() や ReadData() を独自に作らないでください。
コンパイル時のインターフェースチェック
空識別子への代入によって、型がインターフェースを実装していることをコンパイル時に検証します。型定義の近くに配置してください。
var _ io.ReadWriter = (*MyBuffer)(nil)
実行時のコストはありません。MyBuffer が io.ReadWriter を満たさなくなった場合、ビルドはただちに失敗します。
型アサーションと型スイッチ
型アサーションでは必ず comma-ok 形式(s, ok := val.(string))を使用してください。単一値形式では、型が一致しない場合に分岐するのではなくパニックが発生します。動的な型に応じた振り分けには型スイッチを使用し、宣言済みのパラメーター型を広げずに、より豊富な実装を活用するには、小さなオプショナルインターフェースへのアサーション(if f, ok := w.(Flusher); ok)を使用してください。
→ 型スイッチの順序、nil のケース、オプショナルな動作のパターンについては、型アサーションと型スイッチを参照してください。
構造体とインターフェースの埋め込み
構造体の埋め込み
埋め込みを行うと、内部の型のメソッドとフィールドが外側の型に昇格します。これは継承ではなく、コンポジションです。
type Logger struct {
*slog.Logger
}
type Server struct {
Logger
addr string
}
// s.Info(...) works — promoted from slog.Logger through Logger
s := Server{Logger: Logger{slog.Default()}, addr: ":8080"}
s.Info("starting", "addr", s.addr)
昇格したメソッドのレシーバーは外側の型ではなく、_内側_の型です。外側の型で同名のメソッドを定義すれば、上書きできます。
埋め込みと名前付きフィールドの使い分け
| 用途 | 使用する場面 |
|---|---|
| 埋め込み | 内側の型の API 全体を昇格させたい場合。外側の型は拡張された「〜である」型です |
| 名前付きフィールド | 内側の型を内部でのみ使用する場合。外側の型は依存関係を「持つ」型です |
// Embed — Server exposes all http.Handler methods
type Server struct {
http.Handler
}
// Named field — Server uses the store but doesn't expose its methods
type Server struct {
store *DataStore
}
インターフェースを介した依存性注入
コンストラクターでは依存関係をインターフェースとして受け取ります。これによりコンポーネントが疎結合になり、テストが容易になります。
type UserStore interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type UserService struct {
store UserStore
}
func NewUserService(store UserStore) *UserService {
return &UserService{store: store}
}
テストでは、UserStore を満たすモックまたはスタブを渡せばよく、実際のデータベースは必要ありません。
構造体フィールドのタグ
シリアライズされる構造体のエクスポートされたフィールドには、必ずフィールドタグを付けてください。タグがないと、エンコーダーは Go のフィールド名を使用するため、フィールド名の変更によってワイヤー形式が気付かないうちに変わります。
type Order struct {
ID string `json:"id" db:"id"`
Total float64 `json:"total" db:"total"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
Internal string `json:"-" db:"-"`
}
→ タグディレクティブの一覧、omitempty と omitzero の落とし穴、go vet の診断については、構造体フィールド:タグとコピー安全性を参照してください。
ポインタレシーバーと値レシーバー
ポインター (s *Server) を使用する場合 | 値 (s Server) を使用する場合 |
|---|---|
| メソッドがレシーバーを変更する | レシーバーが小さく不変である |
レシーバーに sync.Mutex などが含まれる | レシーバーが基本型(int、string)である |
| レシーバーが大きな構造体である | メソッドが読み取り専用のアクセサーである |
| 一貫性のため、いずれかのメソッドでポインターレシーバーを使う場合は、すべてのメソッドで使う | マップと関数の値(すでに参照型である) |
ある型のすべてのメソッドでレシーバー型を必ず統一してください。あるメソッドでポインターレシーバーを使う場合は、すべてのメソッドで使うべきです。
noCopy による構造体のコピー防止
ミューテックス、チャネル、または内部ポインターを保持する構造体は、コピーすると問題が発生します。コピーによってロック状態が複製され、2 つのゴルーチンがそれぞれ異なるミューテックスを保護するようになり、不変条件が気付かないうちに失われます。noCopy センチネルを埋め込むと、値のコピーをすべて go vet で報告できるため、このような構造体はポインターで渡してください。
診断: 1- go vet ./... — copylocks が、ロックを含む構造体の値コピーを報告します
→ noCopy の実装と vet による検出方法については、構造体フィールド:タグとコピー安全性を参照してください。
相互参照
- → インターフェースの命名規則(Reader、Closer、Stringer)については、
samber/cc-skills-golang@golang-namingスキルを参照してください - → 関数型オプション、コンストラクター、ビルダーパターンについては、
samber/cc-skills-golang@golang-design-patternsスキルを参照してください - → インターフェースを使用した DI パターンについては、
samber/cc-skills-golang@golang-dependency-injectionスキルを参照してください - → 値とポインターの関数パラメーター(レシーバーとは別の概念)については、
samber/cc-skills-golang@golang-code-styleスキルを参照してください - → 安全な名前変更と
implementInterfaceコードアクションについては、samber/cc-skills-golang@golang-goplsスキルを参照してください。インターフェースを満たすメソッドやレシーバーの名前を変更すると、すべての呼び出し箇所が更新され、インターフェースを気付かないうちに壊す名前変更は拒否されます。これは grep/sed では検出できません
よくある間違い
| 間違い | 修正方法 |
|---|---|
| 大きなインターフェース(5 個以上のメソッド) | 1〜3 個のメソッドに絞ったインターフェースに分割し、必要に応じて組み合わせます |
| 実装側のパッケージでインターフェースを定義する | 利用する側で定義します |
| コンストラクターからインターフェースを返す | 具象型を返します |
| comma-ok を使わない型アサーション | 常に v, ok := x.(T) を使います |
| 必要なメソッドが少数なのに埋め込みを使う | 名前付きフィールドを使い、明示的に委譲します |
| シリアライズ対象の構造体でフィールドタグが不足している | マーシャリングする型のエクスポートされたフィールドすべてにタグを付けます |
| 1 つの型でポインタレシーバーと値レシーバーを混在させる | どちらか一方を選び、一貫して使います |
| コンパイル時のインターフェースチェックを忘れる | var _ Interface = (*Type)(nil) を追加します |
String() の代わりに ToString() を使う | 標準のメソッド名に従います |
| 実装が 1 つしかないのに早まってインターフェースを作る | まず具象型から始め、必要になった時点でインターフェースを抽出します |
| ゼロ値の構造体で map/slice が nil のままになっている | メソッド内で遅延初期化します |
型安全な操作に any を使う | 代わりにジェネリクス([T comparable])を使います |