# graphql-go: the schema checks your resolvers before your users do

> graphql-go is a BSD-2-Clause GraphQL server library for Go aiming at full support of the September 2025 GraphQL specification, with a minimal API built on Go method sets. It type-checks resolvers against the schema at parse time, executes them in parallel, converts panics into errors, traces through OpenTelemetry or OpenTracing, and exposes selected-field inspection for prefetching past the N+1 problem.

**graph-gophers/graphql-go** — GraphQL server with a focus on ease of use

- Repository: https://github.com/graph-gophers/graphql-go
- Stars: 4,758 · Forks: 497
- Language: Go
- License: BSD-2-Clause
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/graph-gophers-graphql-go

## The schema checks the resolvers, not the other way around

The stated goal is full support of the September 2025 GraphQL specification with idiomatic, easy to use Go packages, and the distinguishing feature list explains what that means in practice. The API is minimal, a handful of entry points rather than a framework. Schema type-checking runs against resolvers, so a schema field without a matching Go method is an error at schema parse time rather than a runtime surprise. Resolvers are matched to the schema based on method sets, meaning either a Go interface or a Go struct can resolve a GraphQL type. Execution is parallel across resolvers, panics in resolvers are handled rather than fatal, context.Context flows through everything, and the OpenTelemetry and OpenTracing standards are supported for observation. Subscriptions round out the feature list, with a sample WebSocket transport maintained in the companion graphql-transport-ws repository. The September 2025 target is worth pausing on, GraphQL is a moving specification and most server libraries trail it by years, while this project names the current revision as its explicit goal.

## A working server in under thirty lines

The getting started example is a complete server:

```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))
}
```

Run it with go run main.go and query it with curl -XPOST -d '{"query": "{ hello }"}' localhost:8080/query. Three things worth noticing are already visible, the schema is a string parsed at startup, the resolver is an ordinary Go value whose method name matches the field, and the HTTP wiring uses the standard library handler pattern through the relay package rather than a bundled framework. For anything beyond the toy server, the wiki's examples section collects realistic use cases, and the in-repository examples cover the patterns teams actually reach for, caching responses, serving enums, joining an Apollo federation graph, and the starwars schema that exercises deep nesting.

## Methods first, struct fields by option

The resolver contract is spelled out precisely. A resolver must have one method or field for each field of the GraphQL type it resolves, with exported names matching the schema field names case-insensitively. Methods take up to two arguments, an optional context.Context and, when the GraphQL field has arguments, a mandatory pointer to a struct whose exported fields match the argument names, again case-insensitively. Results are up to two as well, the field's value and an optional error. Both shapes are legal:

```go
func (r *helloWorldResolver) Hello() string {
    return "Hello world!"
}
```

```go
func (r *helloWorldResolver) Hello(ctx context.Context) (string, error) {
    return "Hello world!", nil
}
```

Struct fields participate only when the UseFieldResolvers schema option is set, and even then a field is used only when there is no method for it, when it does not implement an interface method, and when it has no arguments, keeping methods the primary resolution path. The case-insensitive matching exists because GraphQL field names are conventionally lowerCamelCase while exported Go identifiers must start uppercase, so hello the schema field and Hello the Go method are the same name under the library's rules without any tagging or code generation to bridge them.

## Query, Mutation and Subscription, separated

A v1.6.0 feature addresses a real spec collision. GraphQL allows the same field name in different operations:

```graphql
schema {
  query: Query
  mutation: Mutation
}

type Query {
  hello: String!
}

type Mutation {
  hello: String!
}
```

would collide on a single root resolver struct, since fields from both operations correspond to methods on the same Go value. The library's answer is optional Query, Mutation and Subscription methods on the root resolver, each returning the resolver for that operation:

```go
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!"
}
```

The separation is opt-in, a single resolver struct still works when no collisions exist, and the operation resolvers are plain values so they can hold state or dependencies like any other struct.

## Guardrail options for untrusted clients

The schema options include the defenses a public endpoint needs. MaxDepth limits field nesting depth, defaulting to 0 which disables the check. MaxQueryLength caps query length in bytes, also defaulting to disabled, and the documentation is blunt that it is highly recommended to set this option when accepting queries from untrusted clients. DisableIntrospection turns off introspection queries entirely when schema disclosure is unwanted. OverlapValidationLimit sets a hard cap on examined overlap pairs during validation, emitting an OverlapValidationLimitExceeded error beyond it, a guard against pathological queries in the validation phase itself. MaxParallelism bounds resolvers running in parallel per request at a default of 10, protecting the process from fan-out explosions. Together these turn the defaults from a development convenience into a configurable production posture. One naming option rounds out the schema surface, UseStringDescriptions enables schema and type system description strings in double and triple quoted form, and when it is not enabled schema comments are parsed as descriptions instead, a small choice that changes how documentation strings survive into introspection.

## Panics become errors, traces are first class

Two operational behaviors are built in rather than delegated. The library handles panics in resolvers, with a PanicHandler option to transform them into errors during query execution, defaulting to errors.DefaultPanicHandler, and a Logger option for logging those panics, defaulting to exec.DefaultLogger, so one misbehaving resolver degrades its own field instead of the process. Tracing is equally native, the Tracer option defaults to a noop but the dependency list shows the real investment, opentracing-go and the OpenTelemetry modules are the only direct dependencies in go.mod, meaning observation support is part of the core rather than an integration to bolt on. Query and field level spans give the shape needed to find slow resolvers inside a single request's parallel execution. The subscription story completes the three operations, with subscriptions.go implementing the operation inside the library and the sample WebSocket transport living in graphql-transport-ws, the split keeping protocol transports out of the core while still giving deployments a working path for streaming responses over the network.

## Selected fields as a prefetch signal

The N+1 problem gets a structural answer, resolvers can inspect the selected fields and their arguments to prefetch data. The inspection helpers expose what the client actually asked for:

```go
graphql.SelectedFieldNames(ctx)       // []string of direct child schema field names
```

returning the direct child field names from the context, alongside a HasSelectedField helper for membership checks, so a resolver for a list can batch load exactly the relations the query will touch instead of triggering one query per row as each child resolver runs. The example directory carries a prefetch example demonstrating the pattern, example_prefetch_test.go tests it at the root, and DisableFieldSelections exists to turn the capture off when the helpers are unused, trading the capability for the overhead. Parallel execution plus informed prefetching is the difference between a GraphQL server that collapses under nested queries and one that does not. Reading the helpers changes resolver economics, a list resolver that knows only two of its six relations were selected can skip loading the other four entirely, which is a saving no amount of downstream caching can recover once the child resolvers have already fired.

## Relay, federation examples, and pooling knobs

The package layout shows a library rather than a framework. The relay package provides the Facebook Relay style handler used in the quick start, time.go and nullable_types.go ship Time and nullable scalar types like NullBool with their own tests, subscriptions.go implements the operation, and errors, log, trace, introspection, ast, decode and types directories hold the internals, with gqltesting supporting test suites. The example directory covers caching, enums, federation, prefetch, social and starwars, federation being the notable one for Apollo federated graphs. Memory behavior is tunable through MaxPooledBufferCap, which caps pooled buffer capacity at a 16KB default with larger buffers discarded, and DisableMemoryPooling for diagnostics and benchmark comparisons against the pooled execution path. Releases are current, v1.10.1 and v1.10.2 in May 2026 and v1.10.3 on 2026-09-22, the same day as the last push, on Go 1.25. The repository root doubles as a test catalogue, with example tests covering custom errors, deprecation, input arrays, null booleans, scalar maps and selection shapes alongside a fuzz test and regression tests filed against issue numbers, a structure that makes behavior easy to verify before depending on it.

## Conclusion

Choose graphql-go when the GraphQL schema should be the contract and Go structs and methods should implement it directly, with the library's compile-time style type-checking catching mismatches between schema and resolvers before a request does, and when its small dependency footprint of essentially the tracing libraries matters. Compare gqlgen's code generation approach when generated types are preferred over method-set binding. Before exposing a server publicly, set MaxQueryLength and MaxDepth since both default to unlimited, decide whether introspection should stay enabled, and wire a tracer other than the default noop so production behavior is observable.

## FAQ

### What is graphql-go?

graphql-go is a BSD-2-Clause GraphQL server library for Go aiming for full support of the September 2025 GraphQL specification with idiomatic packages. It type-checks resolvers against the schema, matches resolvers via Go method sets, executes them in parallel, converts resolver panics into errors, and supports OpenTelemetry and OpenTracing.

### How do you use graphql-go?

Parse your schema string with graphql.MustParseSchema, passing a resolver value whose exported methods match the schema fields, mount the relay.Handler with the schema on an HTTP path, and serve. Query the endpoint with a POST containing the query JSON, and consult the wiki's examples section for realistic cases like federation, caching and prefetching.

### How does graphql-go avoid the N+1 query problem?

Resolvers can inspect the selected fields and their arguments through helpers like graphql.SelectedFieldNames and HasSelectedField, reading from the context which child fields the query will touch, and prefetch exactly that data in one batch instead of loading per row. A prefetch example and its tests ship with the repository.

## Sources

- [graph-gophers/graphql-go on GitHub](https://github.com/graph-gophers/graphql-go)
- [Issues](https://github.com/graph-gophers/graphql-go/issues)
- [License: BSD-2-Clause](https://github.com/graph-gophers/graphql-go/blob/main/LICENSE)
- [README](https://github.com/graph-gophers/graphql-go/blob/main/README.md)
- [Releases](https://github.com/graph-gophers/graphql-go/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/graph-gophers-graphql-go
