The Principal Dev – Masterclass for Tech Leads

The Principal Dev – Masterclass for Tech LeadsNov 27-28

Join

graphql-go Sourcegraph Go Go Report GoDoc

The goal of this project is to provide full support of the October 2021 GraphQL specification with a set of idiomatic, easy to use Go packages.

While still under development (internal APIs are almost certainly subject to change), this library is safe for production use.

Features

(Some) Documentation GoDoc

Getting started

In order to run a simple GraphQL server locally create a main.go file with the following content:

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

Then run the file with go run main.go. To test:

curl -XPOST -d '{"query": "{ hello }"}' localhost:8080/query

For more realistic usecases check our examples section.

Resolvers

A resolver must have one method or field for each field of the GraphQL type it resolves. The method or field name has to be exported and match the schema's field's name in a non-case-sensitive way. You can use struct fields as resolvers by using SchemaOpt: UseFieldResolvers(). For example,

opts := []graphql.SchemaOpt{graphql.UseFieldResolvers()}
schema := graphql.MustParseSchema(s, &query{}, opts...)

When using UseFieldResolvers schema option, a struct field will be used only when:

The method has up to two arguments:

The method has up to two results:

Example for a simple resolver method:

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

The following signature is also allowed:

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

Separate resolvers for different operations

NOTE: This feature is not in the stable release yet. In order to use it you need to run go get github.com/graph-gophers/graphql-go@master and in your go.mod file you will have something like:

v1.5.1-0.20230216224648-5aa631d05992

It is expected to be released in v1.6.0 soon.

The GraphQL specification allows for fields with the same name defined in different query types. For example, the schema below is a valid schema definition:

schema {
  query: Query
  mutation: Mutation
}

type Query {
  hello: String!
}

type Mutation {
  hello: String!
}

The above schema would result in name collision if we use a single resolver struct because fields from both operations correspond to methods in the root resolver (the same Go struct). In order to resolve this issue, the library allows resolvers for query, mutation and subscription operations to be separated using the Query, Mutation and Subscription methods of the root resolver. These special methods are optional and if defined return the resolver for each opeartion. For example, the following is a resolver corresponding to the schema definition above. Note that there is a field named hello in both the query and the mutation definitions:

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)
...

Schema Options

Field Selection Inspection Helpers

Resolvers can introspect which immediate child fields were requested using:

graphql.SelectedFieldNames(ctx)       // []string of direct child schema field names
graphql.HasSelectedField(ctx, "name") // bool
graphql.SortedSelectedFieldNames(ctx) // sorted copy

Use cases include building projection lists for databases or conditionally avoiding expensive sub-fetches. The helpers are intentionally shallow (only direct children) and fragment spreads / inline fragments are flattened with duplicates removed; meta fields (e.g. __typename) are excluded.

Performance: selection data is computed lazily only when a helper is called. If you never call them there is effectively no additional overhead. To remove even the small context value insertion you can opt out with DisableFieldSelections(); helpers then return empty results.

For more detail and examples see the docs.

Custom Errors

Errors returned by resolvers can include custom extensions by implementing the ResolverError interface:

type ResolverError interface {
	error
	Extensions() map[string]interface{}
}

Example of a simple custom error:

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,
	}
}

Which could produce a GraphQL error such as:

{
  "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
}

Tracing

By default the library uses noop.Tracer. If you want to change that you can use the OpenTelemetry or the OpenTracing implementations, respectively:

// OpenTelemetry tracer
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()))
// ...

Alternatively you can pass an existing trace.Tracer instance:

tr := otel.Tracer("example")
_, err = graphql.ParseSchema(starwars.Schema, nil, graphql.Tracer(&otelgraphql.Tracer{Tracer: tr}))
// OpenTracing tracer
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{}))

// ...

If you need to implement a custom tracer the library would accept any tracer which implements the interface below:

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

Examples

Join libs.tech

...and unlock some superpowers

GitHub

We won't share your data with anyone else.