mcpField
G
golang-graphql

golang-graphql

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

golang-graphql について

Go GraphQL エンジニア向けに、スキーマ設計、リゾルバー実装、本番環境の保護策を解説するスキルガイドです。gqlgen と graph-gophers/graphql-go の選択や性能・安全上の課題も扱います。

主な機能

  1. 01Go GraphQL ライブラリと用途の比較
  2. 02SDL、ページネーション、mutation 型の設計
  3. 03リクエスト単位の DataLoader による一括取得
  4. 04認証、認可、安全なエラー処理の実装
  5. 05クエリ複雑度の制限と introspection の管理

活用シーン

  1. 01Go サービスの GraphQL スキーマとリゾルバー設計
  2. 02ネストしたフィールドの N+1 クエリ調査
  3. 03本番環境のクエリ制限と introspection 設定の確認
  4. 04サブスクリプション切断後の goroutine リーク防止

インストール

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

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

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

同梱ファイル

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

evals/
evals.json
references/
gqlgen.md
graphql-go.md
testing.md

詳細

ペルソナ: Go GraphQL エンジニアです。意図を持ってスキーマを設計し、N+1 を防ぐためにデータベースアクセスをバッチ処理し、本番環境ではクエリ複雑度の上限を必須として扱います。

モード:

  • ビルドモード — 新しいスキーマ、リゾルバー、またはサーバー設定を生成する場合:スキルの手順に沿って順番に進めます。新しいコードを生成する前に、バックグラウンドエージェントを起動し、既存のリゾルバーパターンと命名規則を grep で検索します。
  • レビュー�ード — GraphQL コードベースまたは PR を監査する場合:ビジネスロジックを読むのと並行して、サブエージェントを使い、N+1 リゾルバーパターン、未設定の複雑度上限、グローバル DataLoader、本番環境で有効になっているイントロスペクションをスキャンします。

コミュニティのデフォルト。 samber/cc-skills-golang@golang-graphql スキルを明示的に置き換える会社のスキルがある場合は、そちらを優先します。

Go GraphQL のベストプラクティス

主要なライブラリはどちらもスキーマファーストです。SDL(.graphql ファイル)を記述し、Go リゾルバーをバインドします。プロジェクトの規模とチームの好みに応じて選択してください。

このスキルは網羅的なものではありません。最新の API シグネチャについては、各ライブラリの公式ドキュメントとコード例を参照してください。

  • Go パッケージのドキュメント、シンボル、バージョン、インポーター、既知の脆弱性については、Context7 より優先される samber/cc-skills-golang@golang-pkg-go-dev スキル(godig)を参照してください。
  • このライブラリの使用箇所を自身のコード内で調べる(定義、呼び出し箇所、診断)には、samber/cc-skills-golang@golang-gopls スキル(gopls)を参照してください。
  • Context7 は、pkg.go.dev にインデックスされていないドキュメントのフォールバックとして引き続き利用できます。

ライブラリの選択

ライブラリアプローチ型安全性ビルド手順最適な用途
github.com/99designs/gqlgenコード生成コンパイル時go generate大規模なスキーマ、フェデレーション、厳密な型
github.com/graph-gophers/graphql-goリフレクションパース時なしシンプルなスキーマ、高速な反復
github.com/graphql-go/graphqlコードファースト実行時なし避けてください — 冗長で、SDL がありません

次の場合は gqlgen を選びます:Apollo Federation が必要な場合、スキーマが大規模(100 種類以上の型)な場合、または生成されたスタブとリフレクションのオーバーヘッドゼロをチームが望む場合。

次の場合は graph-gophers を選びます:スキーマが小〜中規模の場合、ビルドパイプラインをシンプルに保ちたい場合、または動的なスキーマが必要な場合。

各ライブラリの詳細については、gqlgen リファレンス と graphql-go リファレンス を参照してください。

スキーマ設計

# ✓ Good — explicit nullability; ID scalar for opaque identifiers
type User {
  id: ID!
  email: String! # non-null: the server can always return this
  bio: String # nullable: may be unset
  posts(first: Int = 10, after: String): PostConnection!
}

# ✗ Bad — Int ID leaks implementation details, breaks client caching
type Post {
  id: Int!
}

Nullability のルール: サーバーが常に値を返せる場合に限り、フィールドに ! を付けます。非 null フィールドでリゾルバーエラーが発生すると親オブジェクトが null になり、エラーが連鎖します。null 許容フィールドでは、そのフィールドだけが null になります。

ページネーション: リストフィールドには Relay カーソル接続(Connection/Edge/PageInfo)を使用します。大規模なデータセットではオフセットページネーションを避けてください。カーソルは並行書き込み中も安定しています。

ミューテーション: 結果をエンベロープ型でラップすると、GraphQL の errors 配列を汚さずに、部分的な結果と合わせてビジネスエラーをクライアントに返せます。

type CreateUserPayload {
  user: User
  errors: [UserError!]!
}

リゾルバーパターン

リゾルバーは薄く保ちます。GraphQL の入力をドメイン呼び出しに、ドメインの応答を GraphQL の出力に変換します。

// ✓ Good — resolver delegates to service layer
func (r *mutationResolver) CreateUser(ctx context.Context, input model.CreateUserInput) (*model.CreateUserPayload, error) {
    user, err := r.userService.Create(ctx, input.Email, input.Name)
    if err != nil {
        return nil, formatError(err)
    }
    return &model.CreateUserPayload{User: toGQLUser(user)}, nil
}

// ✗ Bad — SQL in resolver, no separation of concerns
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
    row := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
    // ...
}

すべてのフィールドを扱う単一の巨大なリゾルバーではなく、型ごとのリゾルバー構造体(userResolver、postResolver)を使用します。

N+1 の防止(DataLoader)

バッチ処理を行わない場合、各 User.posts リゾルバーはユーザーごとに SQL クエリを実行するため、n 人のユーザーに対して O(n) 回の DB 呼び出しが発生します。DataLoader はフィールドごとの読み込みをまとめ、単一のバッチクエリにすることでこれを解決します。

重要なルール:DataLoader は HTTP ミドルウェアでリクエストごとに作成しなければなりません。グローバルに作成してはいけません。 グローバルな DataLoader はリクエスト間でキャッシュするため、データが古くなったり、ユーザー間でデータが漏えいしたりする可能性があります。

// ✓ Good — per-request DataLoader in middleware
func DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        loaders := &Loaders{
            PostsByUserID: newPostsByUserIDLoader(r.Context(), db),
        }
        ctx := context.WithValue(r.Context(), loadersKey, loaders)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// ✗ Bad — global DataLoader shared across all requests
var globalLoader = newPostsByUserIDLoader(context.Background(), db)

gqlgen では、gqlgen.yml でバッチ処理するフィールドに resolver: true を指定し、専用のリゾルバーメソッドを必ず使うようにします。DataLoader の設定全体については、gqlgen リファレンス を参照してください。

認証と認可

2 層モデルです。

  1. HTTP ミドルウェア — トークンを抽出して検証し、ID 情報を context.Context に保存します。
  2. スキーマディレクティブ(gqlgen)または リゾルバーチェック(graphql-go)— フィールドごとの認可を適用します。
// HTTP middleware layer (both libraries)
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        user, err := validateToken(token)
        if err != nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), userKey, user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

gqlgen では、フィールドレベルの認可に @hasRole スキーマディレクティブを使用します。認可ポリシーは、リゾルバーに散在させず、スキーマに定義します。gqlgen のリファレンスを参照してください。

エラー処理

内部エラーをそのまま返してはいけません。SQL メッセージ、スタックトレース、サービス内部の情報がクライアントに漏れるおそれがあります。

// gqlgen — custom ErrorPresenter strips internal details
srv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {
    var gqlErr *gqlerror.Error
    if errors.As(err, &gqlErr) {
        return gqlErr // already formatted
    }
    // log internal err here
    return gqlerror.Errorf("internal error") // safe client message
})

// Add extension codes for client-side error handling
return nil, &gqlerror.Error{
    Message: "user not found",
    Extensions: map[string]any{"code": "NOT_FOUND"},
}

graph-gophers では、ResolverError インターフェースを実装して Extensions() を付与します。graphql-go のリファレンスを参照してください。

リゾルバーが部分データを返せる場合、致命的でないフィールドエラーには gqlgen の graphql.AddError(ctx, err) を使用します。

エラーのラップ方法については、samber/cc-skills-golang@golang-error-handling スキルを参照してください。

サブスクリプション

サブスクリプションでは、長時間接続する WebSocket 接続を使用します。重要なのは、常にコンテキストのキャンセルを尊重することです。切断したクライアントごとにゴルーチンがリークすると、気付かないうちにリソースを使い果たします。

// ✓ Good — closes channel when client disconnects
func (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {
    ch := make(chan *model.Message, 1)
    sub := r.pubsub.Subscribe(room) // subscribe once before the goroutine
    go func() {
        defer close(ch) // always close; signals iteration to stop
        for {
            select {
            case <-ctx.Done():
                return // client disconnected
            case msg := <-sub:
                select {
                case ch <- msg:
                case <-ctx.Done():
                    return
                }
            }
        }
    }()
    return ch, nil
}

// ✗ Bad — goroutine leaks forever when client disconnects
func (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {
    ch := make(chan *model.Message, 1)
    go func() {
        for msg := range r.pubsub.Subscribe(room) {
            ch <- msg // blocks forever after client gone
        }
    }()
    return ch, nil
}

パフォーマンスと安全性

本番環境の GraphQL サーバーには明示的な制限が必要です。制限がないと、深くネストされたクエリ 1 つで CPU とメモリを使い果たすおそれがあります。

// gqlgen — wire these into every production handler
srv := handler.NewDefaultServer(es)
srv.Use(extension.FixedComplexityLimit(200)) // max cost per query

// Gate introspection — only in non-production environments
if os.Getenv("ENV") != "production" {
    srv.Use(extension.Introspection{})
}

graph-gophers では、ParseSchema 時に graphql.MaxDepth(10) と graphql.MaxParallelism(10) オプションを指定します。

**クエリの許可リスト:**本番環境では、任意のクエリ文字列を拒否するため、永続化クエリ(gqlgen APQ 拡張機能)の使用を検討してください。

よくある間違い

間違い重要な理由修正方法
子リゾルバーでの N+1 クエリ親行ごとに SQL が 1 回実行されるため、DB 呼び出しが O(n) になりますリクエストごとの DataLoader を使用します
グローバルな DataLoaderリクエスト間でキャッシュが共有され、古いデータやデータ漏えいにつながりますリクエストミドルウェアで DataLoader を作成します
models_gen.go を直接編集する次回の go generate で手動編集が消去されますgqlgen.yml で autobind または models.<T>.model を使用します
スキーマ変更後に go generate を忘れるコンパイル時にリゾルバーインターフェースが一致しなくなりますgo tool gqlgen generate を再実行します
graph-gophers のリゾルバーで int フィールドを使用するライブラリでは Int スカラーに int32 が必要ですint32(Float の場合は float64)を使用します
本番環境でイントロスペクションを有効にする攻撃者にスキーマ全体が公開されますENV チェックで制御します
複雑度の上限がない深くネストされたクエリにより、CPU/メモリを標的とする DoS が発生しますextension.FixedComplexityLimit(N)
リゾルバーから DB エラーが漏れるクライアントに SQL 内部情報が公開されますErrorPresenter/ResolverError でラップします
サブスクリプションのゴルーチンがリークするクライアントが切断しても、ゴルーチンが終了せずに動き続けますdefer close(ch) と select ctx.Done() を使用します
常に必須のデータに nullable フィールドを使うクライアント側であらゆる場所に null チェックが必要になりますスキーマで ! を付け、リゾルバーからエラーを返します

詳細

  • gqlgen のリファレンス — コード生成のワークフロー、gqlgen.yml、DataLoader、Federation v2、ディレクティブ
  • graphql-go のリファレンス — リフレクションを使ったリゾルバーモデル、型のマッピング、トレーシング
  • テスト — gqlgen クライアントハーネス、gqltesting、httptest のパターン

相互参照

  • → リゾルバーとサブスクリプションでのコンテキスト伝播については、samber/cc-skills-golang@golang-context スキルを参照してください
  • → エラーのラップとセンチネルパターンについては、samber/cc-skills-golang@golang-error-handling スキルを参照してください
  • → テーブル駆動テストと統合テストのパターンについては、samber/cc-skills-golang@golang-testing スキルを参照してください
  • → リゾルバーでのトレーシングとメトリクスについては、samber/cc-skills-golang@golang-observability スキルを参照してください
  • → 入力検証とインジェクション防止については、samber/cc-skills-golang@golang-security スキルを参照してください
  • → N+1 クエリのパターンと DataLoader によるデータベースの一括取得については、samber/cc-skills-golang@golang-database スキルを参照してください

参考資料

gqlgen でバグや予期しない動作に遭遇した場合は、https://github.com/99designs/gqlgen/issues で issue を作成してください。

graph-gophers/graphql-go でバグや予期しない動作に遭遇した場合は、https://github.com/graph-gophers/graphql-go/issues で issue を作成してください。

コメント