graphql-go
本项目的目标是通过一组惯用、易用的 Go 包来提供对2021年10月 GraphQL 规范的全面支持。
尽管仍在开发中(internal
API 几乎肯定会发生变化),但该库已经可以安全地用于生产环境。
特性
- 极简 API
- 支持
context.Context
- 支持
OpenTelemetry
和OpenTracing
标准 - 对解析器进行模式类型检查
- 基于方法集将解析器与模式匹配(可以使用 Go 接口或 Go 结构体解析 GraphQL 模式)
- 处理解析器中的 panic
- 并行执行解析器
- 订阅
- 字段上的指令访问器(API 可能在未来版本中发生变化)
(部分)文档
入门
要在本地运行一个简单的 GraphQL 服务器,请创建一个包含以下内容的 main.go
文件:
package main
import (
"log"
"net/http"
graphql "github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/relay"
)
type query struct{}
func (query) Hello() string { return "Hello, world!" }
func main() {
s := `
type Query {
hello: String!
}
`
schema := graphql.MustParseSchema(s, &query{})
http.Handle("/query", &relay.Handler{Schema: schema})
log.Fatal(http.ListenAndServe(":8080", nil))
}
然后使用 go run main.go
运行该文件。测试方法如下:
curl -XPOST -d '{"query": "{ hello }"}' localhost:8080/query
有关更实际的用例,请查看我们的示例部分。
解析器
解析器必须为其解析的 GraphQL 类型的每个字段拥有一个方法或字段。方法或字段名称必须是可导出的,并且与模式的字段名称不区分大小写匹配。
您可以通过使用 SchemaOpt: UseFieldResolvers()
来使用结构体字段作为解析器。例如:
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers()}
schema := graphql.MustParseSchema(s, &query{}, opts...)
使用 UseFieldResolvers
模式选项时,结构体字段仅在以下情况下使用:
- 结构体字段没有对应的方法
- 结构体字段未实现接口方法
- 结构体字段没有参数
该方法最多有两个参数:
- 可选的
context.Context
参数。 - 如果相应的 GraphQL 字段有参数,则必须有
*struct { ... }
参数。结构体字段的名称必须是可导出的,并且必须与 GraphQL 参数的名称不区分大小写匹配。
该方法最多有两个结果:
- 由解析器确定的 GraphQL 字段的值。
- 可选的
error
结果。
简单解析器方法的示例:
func (r *helloWorldResolver) Hello() string {
return "Hello world!"
}
以下签名也是允许的:
func (r *helloWorldResolver) Hello(ctx context.Context) (string, error) {
return "Hello world!", nil
}
不同操作的单独解析器
注意:此功能尚未在稳定版本中发布。要使用它,您需要运行
go get github.com/graph-gophers/graphql-go@master
,在您的go.mod
文件中将会出现类似以下内容:v1.5.1-0.20230216224648-5aa631d05992
预计将在
v1.6.0
版本中发布。
GraphQL 规范允许在不同的查询类型中定义具有相同名称的字段。例如,以下是一个有效的模式定义:
schema {
query: Query
mutation: Mutation
}
type Query {
hello: String!
}
type Mutation {
hello: String!
}
如果我们使用单一的解析器结构体,上述模式会导致名称冲突,因为两种操作中的字段都对应根解析器(同一个Go结构体)中的方法。为了解决这个问题,该库允许使用根解析器的Query
、Mutation
和Subscription
方法将查询、变更和订阅操作的解析器分开。这些特殊方法是可选的,如果定义了,它们会返回每个操作的解析器。例如,以下是对应上述模式定义的解析器。注意,在查询和变更定义中都有一个名为hello
的字段:
type RootResolver struct{}
type QueryResolver struct{}
type MutationResolver struct{}
func(r *RootResolver) Query() *QueryResolver {
return &QueryResolver{}
}
func(r *RootResolver) Mutation() *MutationResolver {
return &MutationResolver{}
}
func (*QueryResolver) Hello() string {
return "Hello query!"
}
func (*MutationResolver) Hello() string {
return "Hello mutation!"
}
schema := graphql.MustParseSchema(sdl, &RootResolver{}, nil)
...
模式选项
UseStringDescriptions()
启用双引号和三引号的使用。未启用时,注释会被解析为描述。UseFieldResolvers()
指定是否使用结构体字段解析器。MaxDepth(n int)
指定查询中字段嵌套的最大深度。默认值为0,表示禁用最大深度检查。MaxParallelism(n int)
指定每个请求允许并行运行的解析器最大数量。默认值为10。Tracer(tracer trace.Tracer)
用于跟踪查询和字段。默认为noop.Tracer
。Logger(logger log.Logger)
用于记录查询执行期间的恐慌。默认为exec.DefaultLogger
。PanicHandler(panicHandler errors.PanicHandler)
用于在查询执行期间将恐慌转换为错误。默认为errors.DefaultPanicHandler
。DisableIntrospection()
禁用内省查询。DirectiveVisitors()
向模式添加指令访问者实现。参见examples/directives/authorization中的示例。
自定义错误
解析器返回的错误可以通过实现ResolverError
接口来包含自定义扩展:
type ResolverError interface {
error
Extensions() map[string]interface{}
}
简单自定义错误的示例:
type droidNotFoundError struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (e droidNotFoundError) Error() string {
return fmt.Sprintf("error [%s]: %s", e.Code, e.Message)
}
func (e droidNotFoundError) Extensions() map[string]interface{} {
return map[string]interface{}{
"code": e.Code,
"message": e.Message,
}
}
这可能会产生如下GraphQL错误:
{
"errors": [
{
"message": "error [NotFound]: This is not the droid you are looking for",
"path": [
"droid"
],
"extensions": {
"code": "NotFound",
"message": "This is not the droid you are looking for"
}
}
],
"data": null
}
跟踪
默认情况下,该库使用noop.Tracer
。如果你想更改,可以分别使用OpenTelemetry或OpenTracing实现:
// OpenTelemetry跟踪器
package main
import (
"github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/example/starwars"
otelgraphql "github.com/graph-gophers/graphql-go/trace/otel"
"github.com/graph-gophers/graphql-go/trace/tracer"
)
// ...
_, err := graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(otelgraphql.DefaultTracer()))
// ...
或者你可以传递一个现有的trace.Tracer实例:
tr := otel.Tracer("example")
_, err = graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(&otelgraphql.Tracer{Tracer: tr}))
// OpenTracing跟踪器
package main
import (
"github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/example/starwars"
"github.com/graph-gophers/graphql-go/trace/opentracing"
"github.com/graph-gophers/graphql-go/trace/tracer"
)
// ...
_, err := graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(opentracing.Tracer{}))
// ...
如果你需要实现自定义跟踪器,该库会接受任何实现以下接口的跟踪器:
type Tracer interface {
TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, func([]*errors.QueryError))
TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, func(*errors.QueryError))
TraceValidation(context.Context) func([]*errors.QueryError)
}