在 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 結構(userResolver、postResolver),而非將所有欄位集中在單一 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 參考。
認證與授權
兩層模型:
- HTTP 中介軟體 — 擷取並驗證 Token,將身分資訊存入
context.Context。 - 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 中使用 autobind 或 models.<T>.model |
Schema 變更後忘記執行 go generate |
編譯時 Resolver 介面不符 | 重新執行 go tool gqlgen generate |
graph-gophers Resolver 中使用 int |
函式庫要求 Int 純量使用 int32 |
使用 int32(Float 則用 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。






