mcpField
G
golang-structs-interfaces

golang-structs-interfaces

github.com/samber開発3.7k40.2k 回インストール5 日前に更新
リポジトリ

golang-structs-interfaces について

開発者やコーディングエージェント向けの Go 型設計スキルです。構造体、インターフェース、ジェネリクス、コンポジションを扱い、利用側で定義する小さなインターフェースと具体型の返却を重視します。

主な機能

  1. 01利用側で小さなインターフェースを定義
  2. 02具体型を返す設計を優先
  3. 03構造体のゼロ値を有用に設計
  4. 04型安全な処理にジェネリクスを使用
  5. 05インターフェース検査と依存性注入を活用

活用シーン

  1. 01利用側パッケージのインターフェースを設計
  2. 02依存性注入でテスト可能なサービスを構築
  3. 03構造体とレシーバーの設計をレビュー
  4. 04コンパイル時にインターフェース実装を確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/samber/cc-skills-golang --skill golang-structs-interfaces

インストール後、Claude Code で/golang-structs-interfacesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
struct-fields.md
type-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 { ... }

標準ライブラリの主要なインターフェース

インターフェースパッケージメソッド
ReaderioRead(p []byte) (n int, err error)
WriterioWrite(p []byte) (n int, err error)
CloserioClose() error
StringerfmtString() string
errorbuiltinError() string
Handlernet/httpServeHTTP(ResponseWriter, *Request)
Marshalerencoding/jsonMarshalJSON() ([]byte, error)
Unmarshalerencoding/jsonUnmarshalJSON([]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])を使います

コメント