golang-graphql

golang-graphql

热门

使用 gqlgen 或 graphql-go 在 Golang 中实现 GraphQL API。适用于构建 GraphQL 服务器、设计模式、编写解析器、处理订阅或将 GraphQL 集成到现有 Go HTTP 服务中。也适用于代码库导入 `github.com/99designs/gqlgen` 或 `github.com/graph-gophers/graphql-go` 的情况。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-graphql
描述

使用 gqlgen 或 graphql-go 在 Golang 中实现 GraphQL API。适用于构建 GraphQL 服务器、设计模式、编写解析器、处理订阅或将 GraphQL 集成到现有 Go HTTP 服务中。也适用于代码库导入 `github.com/99designs/gqlgen` 或 `github.com/graph-gophers/graphql-go` 的情况。

角色: 你是一名 Go GraphQL 工程师。你精心设计模式,批量处理数据库访问以防止 N+1 问题,并将查询复杂度限制视为生产环境中的非可选配置。

模式:

  • 构建模式 — 生成新的模式、解析器或服务器设置:按照技能的逐步说明执行;在生成新代码之前,启动后台代理 grep 现有的解析器模式和命名约定。
  • 审查模式 — 审计 GraphQL 代码库或 PR:使用子代理扫描 N+1 解析器模式、缺失的复杂度上限、全局 DataLoader 以及生产环境中启用的 introspection,同时并行读取业务逻辑。

社区默认。 明确取代 samber/cc-skills-golang@golang-graphql 技能的公司技能具有优先权。

Go GraphQL 最佳实践

两个主要库都是模式优先:编写 SDL(.graphql 文件),绑定 Go 解析器。根据项目规模和团队偏好选择。

本技能并非详尽无遗。请参考每个库的官方文档和代码示例以获取最新的 API 签名。Context7 可作为发现平台提供帮助。有关 Go 包文档、版本、符号和已知漏洞,请参见 samber/cc-skills-golang@golang-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 参考

模式设计

# ✓ 好 — 显式可空性;ID 标量用于不透明标识符
type User {
  id: ID!
  email: String! # 非空:服务器始终可以返回此值
  bio: String # 可空:可能未设置
  posts(first: Int = 10, after: String): PostConnection!
}

# ✗ 差 — Int ID 泄露实现细节,破坏客户端缓存
type Post {
  id: Int!
}

可空性规则: 仅当服务器可以 始终 返回值时,才将字段标记为 !。非空字段上的解析器错误会使父对象变为 null,导致级联失败;可空字段仅使该字段本身变为 null。

分页: 对列表字段使用 Relay 游标连接(Connection/Edge/PageInfo)。避免在大数据集上使用偏移分页 — 游标在并发写入下是稳定的。

变更: 将结果包装在信封类型中,以便客户端接收业务错误和部分结果,而不会污染 GraphQL errors 数组:

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

解析器模式

保持解析器精简 — 它们将 GraphQL 输入转换为领域调用,并将领域响应转换为 GraphQL 输出。

// ✓ 好 — 解析器委托给服务层
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
}

// ✗ 差 — 解析器中包含 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)
    // ...
}

使用按类型解析器结构体(userResolverpostResolver),而不是为所有字段使用一个单一的解析器。

N+1 预防(DataLoader)

每个 User.posts 解析器在没有批处理的情况下为每个用户触发一个 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,以强制使用专用的解析器方法。有关完整的 DataLoader 接线,请参见 gqlgen 参考

身份验证与授权

两层模型:

  1. HTTP 中间件 — 提取并验证令牌,将身份信息存入 context.Context
  2. 模式指令(gqlgen)或 解析器检查(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 模式指令进行字段级授权 — 授权策略存在于模式中,而不是分散在解析器中。请参见 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 中,对于解析器仍可返回部分数据的非致命字段错误,使用 graphql.AddError(ctx, err)

有关错误包装模式,请参见 samber/cc-skills-golang@golang-error-handling 技能。

订阅

订阅使用长连接 WebSocket。关键纪律:始终尊重上下文取消 — 每个断开连接的客户端泄漏的 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 扩展)以拒绝任意查询字符串。

常见错误

错误 为什么重要 修复
子解析器中的 N+1 查询 每个父行一个 SQL → O(n) 次数据库调用 使用按请求的 DataLoader
全局 DataLoader 跨请求缓存 — 数据过时、数据泄露 在请求中间件中创建 DataLoader
直接编辑 models_gen.go 下次 go generate 会清除手动编辑 gqlgen.yml 中使用 autobindmodels.<T>.model
模式更改后忘记 go generate 编译时解析器接口不匹配 重新运行 go tool gqlgen generate
graph-gophers 解析器中的 int 字段 库要求 int32 用于 Int 标量 使用 int32(或 float64 用于 Float
生产环境中启用 introspection 向攻击者暴露完整模式 使用 ENV 检查进行门控
没有复杂度上限 深度嵌套查询 → CPU/内存 DoS extension.FixedComplexityLimit(N)
从解析器泄露数据库错误 向客户端暴露 SQL 内部信息 包装在 ErrorPresenter / ResolverError
订阅 goroutine 泄漏 客户端断开 → goroutine 永远运行 defer close(ch) + select ctx.Done()
对始终需要的数据使用可空字段 客户端必须到处进行空检查 在模式中标记 !;从解析器返回错误

深入探讨

  • 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 技能,了解输入验证和注入预防
  • → 参见 samber/cc-skills-golang@golang-database 技能,了解 N+1 查询模式和 DataLoader 数据库批处理

参考

如果你遇到 gqlgen 的 bug 或意外行为,请在 https://github.com/99designs/gqlgen/issues 提交 issue。

如果你遇到 graph-gophers/graphql-go 的 bug 或意外行为,请在 https://github.com/graph-gophers/graphql-go/issues 提交 issue。