gqlgen es el generador de servidores GraphQL en Go más popular y su enfoque es schema-first: defines el esquema en SDL y gqlgen genera las interfaces Go que tienes que implementar. Esto garantiza que la API y el código de Go nunca se desincronicen.
Inicialización con gqlgen
go get github.com/99designs/gqlgen go run github.com/99designs/gqlgen init
El comando init crea la estructura básica: graph/schema.graphqls, graph/resolver.go, gqlgen.yml y server.go. Cada vez que modifiques el schema ejecuta go run github.com/99designs/gqlgen generate para regenerar el código.
Definir el schema
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
}
type Query {
user(id: ID!): User
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(authorId: ID!, title: String!, body: String!): Post!
}
type Subscription {
postCreated: Post!
}
Implementar los resolvers
gqlgen genera una interfaz por cada tipo con campos resolubles. La implementas en graph/resolver.go:
type Resolver struct {
users map[string]*model.User
posts map[string]*model.Post
mu sync.RWMutex
postSubs []chan *model.Post
}
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
u, ok := r.users[id]
if !ok {
return nil, fmt.Errorf("usuario %s no encontrado", id)
}
return u, nil
}
func (r *queryResolver) Posts(ctx context.Context) ([]*model.Post, error) {
r.mu.RLock()
defer r.mu.RUnlock()
posts := make([]*model.Post, 0, len(r.posts))
for _, p := range r.posts {
posts = append(posts, p)
}
return posts, nil
}
Mutations
func (r *mutationResolver) CreateUser(ctx context.Context, name, email string) (*model.User, error) {
r.mu.Lock()
defer r.mu.Unlock()
u := &model.User{
ID: fmt.Sprintf("u%d", len(r.users)+1),
Name: name,
Email: email,
}
r.users[u.ID] = u
return u, nil
}
func (r *mutationResolver) CreatePost(ctx context.Context, authorID, title, body string) (*model.Post, error) {
r.mu.Lock()
defer r.mu.Unlock()
p := &model.Post{
ID: fmt.Sprintf("p%d", len(r.posts)+1),
Title: title,
Body: body,
Author: r.users[authorID],
}
r.posts[p.ID] = p
// notificar subscripciones
go func() {
for _, ch := range r.postSubs {
ch <- p
}
}()
return p, nil
}
El problema N+1 y dataloaders
Cuando GraphQL resuelve una lista de posts y para cada uno carga el autor por separado, ejecuta N+1 queries a la base de datos. El dataloader agrupa esas peticiones individuales en una sola query:
// go get github.com/graph-gophers/dataloader/v7
type Loaders struct {
UserByID *dataloader.Loader[string, *model.User]
}
func NewLoaders(db *sql.DB) *Loaders {
return &Loaders{
UserByID: dataloader.NewBatchedLoader(func(ctx context.Context, ids []string) []*dataloader.Result[*model.User] {
// una sola query para todos los ids
users := cargarUsuariosPorIds(db, ids)
results := make([]*dataloader.Result[*model.User], len(ids))
for i, id := range ids {
if u, ok := users[id]; ok {
results[i] = &dataloader.Result[*model.User]{Data: u}
} else {
results[i] = &dataloader.Result[*model.User]{Error: fmt.Errorf("user %s not found", id)}
}
}
return results
}),
}
}
// En el resolver de Post.Author:
func (r *postResolver) Author(ctx context.Context, obj *model.Post) (*model.User, error) {
return loaders.UserByID.Load(ctx, obj.AuthorID)()
}
Subscriptions en tiempo real
func (r *subscriptionResolver) PostCreated(ctx context.Context) (<-chan *model.Post, error) {
ch := make(chan *model.Post, 1)
r.mu.Lock()
r.postSubs = append(r.postSubs, ch)
r.mu.Unlock()
go func() {
<-ctx.Done() // cliente desconectado
r.mu.Lock()
for i, sub := range r.postSubs {
if sub == ch {
r.postSubs = append(r.postSubs[:i], r.postSubs[i+1:]...)
break
}
}
r.mu.Unlock()
close(ch)
}()
return ch, nil
}
gqlgen usa WebSockets o Server-Sent Events para las subscriptions. El servidor HTTP de server.go generado ya configura el handler correcto si incluyes el handler de subscriptions en el playground.
