mcpField
G
golang-graphql

golang-graphql

github.com/samber开发3.7k37.4k 次安装5 天前更新
代码仓库

关于 golang-graphql

面向 Go GraphQL 工程师的技能指南,讲解 schema 设计、resolver 实现与生产环境防护。覆盖 gqlgen 和 graph-gophers/graphql-go 的选型及常见性能、安全问题。

主要功能

  1. 01比较 Go GraphQL 库及适用场景
  2. 02设计 SDL、分页和 mutation 结构
  3. 03使用按请求创建的 DataLoader 批处理
  4. 04实现认证、授权与安全错误处理
  5. 05限制查询复杂度并管理 introspection

适用场景

  1. 01为 Go 服务设计 GraphQL schema 与 resolver
  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,并将查询复杂度限制视为生产环境中的必选项。

模式:

  • 构建模式 — 生成新架构、解析器或服务端设置时:遵循技能的顺序说明;在生成新代码前,启动后台 Agent 搜索现有解析器模式和命名惯例。
  • 审查模式 — 审查 GraphQL 代码库或 PR 时:使用子 Agent 扫描 N+1 解析器模式、缺失的复杂度上限、全局 DataLoader,以及生产环境中启用的内省,同时并行阅读业务逻辑。

社区默认规则。 如果公司技能明确取代了 samber/cc-skills-golang@golang-graphql 技能,则以公司技能为准。

Go GraphQL 最佳实践

两大主流库都采用架构优先的方式:编写 SDL(.graphql 文件),并绑定 Go 解析器。请根据项目规模和团队偏好进行选择。

本技能并未涵盖所有内容——有关当前 API 签名,请参阅各库的官方文档和代码示例:

  • 有关 Go 包文档、符号、版本、导入方和已知漏洞,→ 请参阅 samber/cc-skills-golang@golang-pkg-go-dev 技能(godig);查询 Go 包信息时优先于 Context7。
  • 要在自己的代码中导航此库的用法(定义、调用位置、诊断信息),→ 请参阅 samber/cc-skills-golang@golang-gopls 技能(gopls)。
  • 对于 pkg.go.dev 未收录的文档,Context7 仍可作为备用方案。

库的选择

库方式类型安全构建步骤最适用于
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!
}

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

分页: 列表字段使用 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) 次数据库调用。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 参考。

身份验证与授权

两层模型:

  1. HTTP 中间件 — 提取并验证令牌,将身份存入 context.Context。
  2. Schema 指令(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 Schema 指令进行字段级授权——授权策略位于 Schema 中,而不是散落在各个解析器里。参见 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。关键原则:始终遵循上下文取消——每个断开连接的客户端都会泄漏一个 goroutine,悄无声息地耗尽资源。

// ✓ 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 服务需要明确的限制。否则,单个深度嵌套的查询就会耗尽 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 → O(n) 次数据库调用使用每请求一个的 DataLoader
全局 DataLoader跨请求缓存——数据过期、数据泄漏在请求中间件中创建 DataLoader
直接编辑 models_gen.go下次 go generate 会覆盖手动修改在 gqlgen.yml 中使用 autobind 或 models.<T>.model
Schema 更改后忘记运行 go generate编译时解析器接口不匹配重新运行 go tool gqlgen generate
graph-gophers 解析器中使用 int 字段库要求 Int 标量使用 int32使用 int32(或为 Float 使用 float64)
在生产环境启用内省向攻击者暴露完整 Schema使用 ENV 检查控制访问
未限制复杂度深度嵌套的查询 → CPU/内存 DoSextension.FixedComplexityLimit(N)
从解析器泄露数据库错误向客户端暴露 SQL 内部信息使用 ErrorPresenter / ResolverError 包装
订阅 goroutine 泄漏客户端断开连接 → goroutine 永远运行defer close(ch) + select ctx.Done()
始终必需的数据使用可空字段客户端必须到处进行 null 检查在 Schema 中标记 !;从解析器返回错误

深入了解

  • 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 提交问题。

如果你在 graph-gophers/graphql-go 中遇到错误或意外行为,请在 https://github.com/graph-gophers/graphql-go/issues 提交问题。

评论

支持 Markdown