golang-graphql

golang-graphql

熱門

在 Golang 中使用 gqlgen 或 graphql-go 實作 GraphQL API。適用於建置 GraphQL 伺服器、設計 Schema、撰寫 Resolver、處理 Subscription,或將 GraphQL 整合至現有 Go HTTP 服務。也適用於程式碼匯入 `github.com/99designs/gqlgen` 或 `github.com/graph-gophers/graphql-go` 的情況。

2261星標
150分支
更新於 2026/6/6
SKILL.md
readonlyread-only
name
golang-graphql
description

在 Golang 中使用 gqlgen 或 graphql-go 實作 GraphQL API。適用於建置 GraphQL 伺服器、設計 Schema、撰寫 Resolver、處理 Subscription,或將 GraphQL 整合至現有 Go HTTP 服務。也適用於程式碼匯入 `github.com/99designs/gqlgen` 或 `github.com/graph-gophers/graphql-go` 的情況。

角色定位: 你是一位 Go GraphQL 工程師。你刻意設計 Schema、批次處理資料庫存取以避免 N+1 問題,並將查詢複雜度限制視為生產環境中不可省略的項目。

模式:

  • 建置模式 — 產生新的 Schema、Resolver 或伺服器設定:遵循技能的逐步指示;在產生新程式碼前,啟動背景代理程式搜尋現有的 Resolver 模式與命名慣例。
  • 審查模式 — 稽核 GraphQL 程式碼庫或 Pull Request:使用子代理程式掃描 N+1 Resolver 模式、遺漏的複雜度上限、全域 DataLoader 以及生產環境中啟用的 Introspection,同時並行閱讀業務邏輯。

社群預設值。 明確取代 samber/cc-skills-golang@golang-graphql 技能的團隊技能具有優先權。

Go GraphQL 最佳實務

兩個主要函式庫皆為 Schema-first:撰寫 SDL(.graphql 檔案),綁定 Go Resolver。根據專案規模與團隊偏好選擇。

此技能並非詳盡無遺。請參閱各函式庫的官方文件與程式碼範例以取得最新的 API 簽章。Context7 可作為探索平台提供協助。如需 Go 套件文件、版本、符號與已知漏洞,請參閱 samber/cc-skills-golang@golang-pkg-go-dev 技能。

函式庫選擇

函式庫 方式 型別安全 建置步驟 最適合
github.com/99designs/gqlgen 程式碼產生 編譯時期 go generate 大型 Schema、Federation、嚴謹型別
github.com/graph-gophers/graphql-go 反射 解析時期 簡單 Schema、快速迭代
github.com/graphql-go/graphql Code-first 執行時期 避免使用 — 冗長、無 SDL

選擇 gqlgen 的情況:需要 Apollo Federation、Schema 龐大(超過 100 個型別)、或團隊希望產生 stub 且無反射開銷。

選擇 graph-gophers 的情況:Schema 小型/中型、希望保持建置管線簡單、或需要動態 Schema。

如需深入了解各函式庫,請參閱 gqlgen 參考graphql-go 參考

Schema 設計

# ✓ 良好 — 明確的可空性;使用 ID 純量作為不透明識別碼
type User {
  id: ID!
  email: String! # 非空:伺服器永遠可以回傳此值
  bio: String # 可空:可能未設定
  posts(first: Int = 10, after: String): PostConnection!
}

# ✗ 不良 — Int ID 洩漏實作細節,破壞客戶端快取
type Post {
  id: Int!
}

可空性規則: 僅在伺服器永遠能回傳值時,才將欄位標記為 !。非空欄位的 Resolver 錯誤會使父物件變為 null,導致串聯失敗;可空欄位僅使該欄位本身變為 null。

分頁: 對於列表欄位,使用 Relay 游標連線(Connection/Edge/PageInfo)。避免在大型資料集上使用偏移分頁 — 游標在並行寫入下保持穩定。

Mutation: 將結果包裝在信封型別中,讓客戶端能在不污染 GraphQL errors 陣列的情況下,接收業務錯誤與部分結果:

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

Resolver 模式

保持 Resolver 輕薄 — 它們將 GraphQL 輸入轉換為領域呼叫,並將領域回應轉換為 GraphQL 輸出。

// ✓ 良好 — Resolver 委派給服務層
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
}

// ✗ 不良 — Resolver 中直接寫 SQL,缺乏關注點分離
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
    row := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
    // ...
}

使用每個型別各自的 Resolver 結構(userResolverpostResolver),而非將所有欄位集中在單一 Resolver 中。

N+1 預防(DataLoader)

每個 User.posts Resolver 若未批次處理,會對每個使用者觸發一次 SQL 查詢 — 對 n 個使用者產生 O(n) 次資料庫呼叫。DataLoader 透過將每個欄位的載入合併為單一批次查詢來解決此問題。

關鍵規則:DataLoader 必須在 HTTP 中介軟體中每個請求建立一次,絕不能是全域的。 全域 DataLoader 會跨請求快取 — 導致資料過時,甚至可能跨使用者資料洩漏。

// ✓ 良好 — 中介軟體中每個請求的 DataLoader
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))
    })
}

// ✗ 不良 — 全域 DataLoader 在所有請求間共用
var globalLoader = newPostsByUserIDLoader(context.Background(), db)

在 gqlgen 中,在 gqlgen.yml 中將批次欄位標記為 resolver: true,以強制產生專用的 Resolver 方法。完整的 DataLoader 接線方式請參閱 gqlgen 參考

認證與授權

兩層模型:

  1. HTTP 中介軟體 — 擷取並驗證 Token,將身分資訊存入 context.Context
  2. Schema 指令(gqlgen)Resolver 檢查(graphql-go) — 強制執行每個欄位的授權。
// HTTP 中介軟體層(兩個函式庫皆適用)
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 Schema 指令進行欄位層級的授權 — 授權策略存在 Schema 中,而非散落在各 Resolver 中。請參閱 gqlgen 參考

錯誤處理

絕不回傳原始的內部錯誤 — 它們會向客戶端洩漏 SQL 訊息、堆疊追蹤或服務內部細節。

// gqlgen — 自訂 ErrorPresenter 移除內部細節
srv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {
    var gqlErr *gqlerror.Error
    if errors.As(err, &gqlErr) {
        return gqlErr // 已格式化
    }
    // 在此記錄內部錯誤
    return gqlerror.Errorf("internal error") // 安全的客戶端訊息
})

// 為客戶端錯誤處理加入擴充代碼
return nil, &gqlerror.Error{
    Message: "user not found",
    Extensions: map[string]any{"code": "NOT_FOUND"},
}

對於 graph-gophers,實作 ResolverError 介面以附加 Extensions()。請參閱 graphql-go 參考

在 gqlgen 中,對於非致命的欄位錯誤(Resolver 仍可回傳部分資料),使用 graphql.AddError(ctx, err)

關於錯誤包裝模式,請參閱 samber/cc-skills-golang@golang-error-handling 技能。

Subscription

Subscription 使用長連線的 WebSocket 連線。關鍵紀律:永遠尊重 context 取消 — 每個斷線客戶端遺漏的 goroutine 會默默耗盡資源。

// ✓ 良好 — 客戶端斷線時關閉通道
func (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {
    ch := make(chan *model.Message, 1)
    sub := r.pubsub.Subscribe(room) // 在 goroutine 前先訂閱一次
    go func() {
        defer close(ch) // 永遠關閉;通知迭代停止
        for {
            select {
            case <-ctx.Done():
                return // 客戶端已斷線
            case msg := <-sub:
                select {
                case ch <- msg:
                case <-ctx.Done():
                    return
                }
            }
        }
    }()
    return ch, nil
}

// ✗ 不良 — 客戶端斷線時 goroutine 永遠遺漏
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 // 客戶端離開後永遠阻塞
        }
    }()
    return ch, nil
}

效能與安全性

生產環境的 GraphQL 伺服器需要明確的限制。若無限制,單一深度巢狀查詢就會耗盡 CPU 與記憶體。

// gqlgen — 將這些接入每個生產環境的處理器
srv := handler.NewDefaultServer(es)
srv.Use(extension.FixedComplexityLimit(200)) // 每次查詢的最大成本

// 閘控 Introspection — 僅在非生產環境中啟用
if os.Getenv("ENV") != "production" {
    srv.Use(extension.Introspection{})
}

對於 graph-gophers:在 ParseSchema 時使用 graphql.MaxDepth(10)graphql.MaxParallelism(10) 選項。

查詢允許清單: 在生產環境中,考慮使用持久化查詢(gqlgen APQ 擴充)以拒絕任意查詢字串。

常見錯誤

錯誤 為何重要 修正方式
子 Resolver 中的 N+1 查詢 每個父資料列一個 SQL → O(n) 次資料庫呼叫 使用每個請求的 DataLoader
全域 DataLoader 跨請求快取 — 資料過時、資料洩漏 在請求中介軟體中建立 DataLoader
直接編輯 models_gen.go 下次 go generate 會清除手動編輯 gqlgen.yml 中使用 autobindmodels.<T>.model
Schema 變更後忘記執行 go generate 編譯時 Resolver 介面不符 重新執行 go tool gqlgen generate
graph-gophers Resolver 中使用 int 函式庫要求 Int 純量使用 int32 使用 int32Float 則用 float64
生產環境中啟用 Introspection 向攻擊者暴露完整 Schema 使用 ENV 檢查閘控
無複雜度上限 深度巢狀查詢 → CPU/記憶體 DoS extension.FixedComplexityLimit(N)
從 Resolver 洩漏資料庫錯誤 向客戶端暴露 SQL 內部資訊 包裝在 ErrorPresenter / ResolverError
Subscription goroutine 遺漏 客戶端斷線 → goroutine 永遠執行 defer close(ch) + select ctx.Done()
對永遠需要的資料使用可空欄位 客戶端必須到處進行 null 檢查 在 Schema 中標記 !;從 Resolver 回傳錯誤

深入探討

  • gqlgen 參考 — 程式碼產生工作流程、gqlgen.yml、DataLoader、Federation v2、指令
  • graphql-go 參考 — 反射 Resolver 模型、型別對應、追蹤
  • 測試 — gqlgen 客戶端 harness、gqltesting、httptest 模式

交叉參考

  • → 請參閱 samber/cc-skills-golang@golang-context 技能,了解 Resolver 與 Subscription 中的 context 傳遞
  • → 請參閱 samber/cc-skills-golang@golang-error-handling 技能,了解錯誤包裝與哨兵模式
  • → 請參閱 samber/cc-skills-golang@golang-testing 技能,了解表格驅動與整合測試模式
  • → 請參閱 samber/cc-skills-golang@golang-observability 技能,了解 Resolver 中的追蹤與指標
  • → 請參閱 samber/cc-skills-golang@golang-security 技能,了解輸入驗證與注入防範
  • → 請參閱 samber/cc-skills-golang@golang-database 技能,了解 N+1 查詢模式與 DataLoader 資料庫批次處理

參考資料

若你在 gqlgen 中遇到錯誤或非預期行為,請在 https://github.com/99designs/gqlgen/issues 開啟 issue。

若你在 graph-gophers/graphql-go 中遇到錯誤或非預期行為,請在 https://github.com/graph-gophers/graphql-go/issues 開啟 issue。