golang-graphql について
Go GraphQL エンジニア向けに、スキーマ設計、リゾルバー実装、本番環境の保護策を解説するスキルガイドです。gqlgen と graph-gophers/graphql-go の選択や性能・安全上の課題も扱います。
主な機能
- 01Go GraphQL ライブラリと用途の比較
- 02SDL、ページネーション、mutation 型の設計
- 03リクエスト単位の DataLoader による一括取得
- 04認証、認可、安全なエラー処理の実装
- 05クエリ複雑度の制限と introspection の管理
活用シーン
- 01Go サービスの GraphQL スキーマとリゾルバー設計
- 02ネストしたフィールドの N+1 クエリ調査
- 03本番環境のクエリ制限と introspection 設定の確認
- 04サブスクリプション切断後の goroutine リーク防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ samber/ cc-skills-golang --skill golang-graphqlインストール後、Claude Code で/golang-graphqlを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/gqlgen.mdgraphql-go.mdtesting.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 層モデルです。
- HTTP ミドルウェア — トークンを抽出して検証し、ID 情報を
context.Contextに保存します。 - スキーマディレクティブ(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 を作成してください。