Aufbau eines typsicheren Schema-First GraphQL-Servers in Go mit gqlgen
Emily Parker
Product Engineer · Leapcell

Der Aufbau einer robusten und wartbaren API ist ein Eckpfeiler der modernen Softwareentwicklung. Wenn die Komplexität von Anwendungen zunimmt, werden die Notwendigkeit effizienter Datenabrufe, klarer Verträge zwischen Client und Server sowie vereinfachter Entwicklungsworkflows von größter Bedeutung. Traditionell waren REST-APIs eine beliebte Wahl, aber sie stellen oft Herausforderungen wie Over-Fetching, Under-Fetching und die Komplexität der Verwaltung mehrerer Endpunkte dar. Hier glänzt GraphQL und bietet eine flexible und leistungsstarke Alternative. Allein die Verwendung von GraphQL reicht jedoch nicht aus. Um seine Vorteile wirklich nutzen zu können, insbesondere in einer stark typisierten Sprache wie Go, ist die Einführung eines typsicheren Schema-First-Ansatzes entscheidend. Dieser Artikel führt Sie durch den Prozess des Aufbaus eines solchen Servers mit gqlgen
, einem leistungsstarken Tool, das das Beste von GraphQL in das Go-Ökosystem bringt.
Die Kernkonzepte verstehen
Bevor wir uns mit der Implementierung befassen, wollen wir die grundlegenden Konzepte, die unserer Serverentwicklung zugrunde liegen, klar verstehen.
GraphQL: Im Wesentlichen ist GraphQL eine Abfragesprache für Ihre API und eine Laufzeitumgebung zur Erfüllung dieser Abfragen mit Ihren vorhandenen Daten. Im Gegensatz zu REST, wo Sie in der Regel mehrere Endpunkte aufrufen, um Daten zu sammeln, ermöglicht GraphQL Clients, genau die Daten anzufordern, die sie benötigen, in einer einzigen Abfrage, die hierarchisch strukturiert ist. Dies minimiert Netzwerkanfragen und Over-Fetching.
Schema-First-Entwicklung: Dieses Paradigma betont die Definition Ihres GraphQL-Schemas als einzige Quelle der Wahrheit für Ihre API. Sie schreiben zuerst das Schema in der Schema Definition Language (SDL) von GraphQL und geben alle Typen, Felder und Operationen (Queries, Mutations, Subscriptions) an, die Ihre API unterstützt. Tools wie gqlgen
verwenden dieses Schema dann, um wesentliche Teile Ihres serverseitigen Codes zu generieren und sicherzustellen, dass Ihre Implementierung strikt dem definierten Vertrag entspricht. Dieser Ansatz fördert die klare Kommunikation zwischen Frontend- und Backend-Teams und vereinfacht die API-Entwicklung.
Typsicherheit: In stark typisierten Sprachen wie Go geht es bei der Typsicherheit darum, sicherzustellen, dass Variablen und Ausdrücke zur Kompilierzeit einen klar definierten Typ haben, wodurch typbezogene Fehler vermieden und der Code vorhersehbarer und wartbarer wird. In Kombination mit der Schema-First-Entwicklung nutzt gqlgen
die Typdefinitionen des GraphQL-Schemas, um Go-Structs und Schnittstellen zu generieren, die Ihre GraphQL-Typen effektiv auf Go-Typen abbilden. Dies bietet Ende-zu-Ende-Typsicherheit, von der Client-Abfrage bis zu Ihren Go-Resolver-Funktionen, und fängt potenzielle Probleme früh im Entwicklungszyklus ab.
gqlgen
: Dies ist eine Go-Bibliothek, die einen GraphQL-Server aus einem GraphQL-Schema generiert. Sie ist stark auf die Schema-First-Entwicklung ausgerichtet und konzentriert sich auf die Bereitstellung einer leistungsstarken und flexiblen Code-Generierungs-Engine, die es Entwicklern ermöglicht, sich auf die Implementierung von Geschäftslogik anstelle von Boilerplate-Code zu konzentrieren.
Erstellen Ihres GraphQL-Servers mit gqlgen
Lassen Sie uns den Aufbau einer einfachen GraphQL-API zur Verwaltung einer Liste von Todo
-Elementen durchgehen.
Einrichten Ihres Projekts
Stellen Sie zunächst sicher, dass Sie Go installiert haben. Erstellen Sie dann ein neues Go-Modul:
mkdir todo-graphql-server cd todo-graphql-server go mod init todo-graphql-server
Installieren Sie dann gqlgen
und seine Abhängigkeiten:
go get github.com/99designs/gqlgen go get github.com/99designs/gqlgen/cmd@latest
Initialisieren Sie nun gqlgen
in Ihrem Projekt. Dies erstellt wichtige Dateien: gqlgen.yml
, graph/schema.resolvers.go
, graph/schema.graphqls
und graph/generated.go
.
go run github.com/99designs/gqlgen init
Definieren des GraphQL-Schemas
Öffnen Sie graph/schema.graphqls
. Diese Datei enthält unsere GraphQL-Schema-Definition. Definieren wir einen Todo
-Typ und die grundlegenden Abfragen und Mutationen:
# graph/schema.graphqls type Todo { id: ID! text: String! done: Boolean! user: User! } type User { id: ID! name: String! } type Query { todos: [Todo!]! } type Mutation { createTodo(text: String!, userId: ID!): Todo! markTodoDone(id: ID!): Todo }
Nachdem Sie das Schema aktualisiert haben, führen Sie gqlgen generate
aus, um den generierten Go-Code zu aktualisieren:
go run github.com/99designs/gqlgen generate
Dieser Befehl aktualisiert graph/generated.go
und graph/model/models_gen.go
. models_gen.go
enthält nun Go-Structs, die unsere Todo
- und User
-Typen und Eingabetypen darstellen, falls definiert. Zum Beispiel:
// graph/model/models_gen.go package model type NewTodo struct { Text string `json:"text"` UserID string `json:"userId"` } type Todo struct { ID string `json:"id"` Text string `json:"text"` Done bool `json:"done"` User *User `json:"user"` } type User struct { ID string `json:"id"` Name string `json:"name"` }
Beachten Sie, wie gqlgen
den Eingabetyp NewTodo
für unsere createTodo
-Mutation basierend auf ihren Argumenten automatisch ableitet.
Implementieren von Resolvers
Die generierte Datei graph/schema.resolvers.go
enthält ein grundlegendes Gerüst für unsere Resolver. Resolver sind Funktionen, die für den Abruf der Daten für ein bestimmtes Feld im Schema verantwortlich sind.
Lassen Sie uns graph/schema.resolvers.go
ändern, um die Logik für createTodo
, todos
und markTodoDone
zu implementieren. Der Einfachheit halber verwenden wir einen In-Memory-Speicher für unsere Daten.
Definieren Sie zunächst unseren Datenspeicher und eine Möglichkeit zur Generierung eindeutiger IDs, typischerweise in graph/resolver.go
:
// graph/resolver.go package graph import ( "context" "fmt" "math/rand" "sync" "time" "todo-graphql-server/graph/model" ) // This file will not be regenerated automatically. // // It serves as dependency injection for your app, add any dependencies you require here. type Resolver struct { mu sync.Mutex todos []*model.Todo users []*model.User } func init() { rand.Seed(time.Now().UnixNano()) } var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randString(n int) string { b := make([]rune, n) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes))] } return string(b) } func (r *Resolver) GetUserByID(id string) *model.User { for _, user := range r.users { if user.ID == id { return user } } return nil }
Füllen wir nun die Resolver-Logik in graph/schema.resolvers.go
:
// graph/schema.resolvers.go package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. // Code generated by github.com/99designs/gqlgen version v0.17.45 import ( "context" "fmt" "todo-graphql-server/graph/model" ) // CreateTodo is the resolver for the createTodo field. func (r *mutationResolver) CreateTodo(ctx context.Context, text string, userID string) (*model.Todo, error) { r.mu.Lock() defer r.mu.Unlock() user := r.GetUserByID(userID) if user == nil { return nil, fmt.Errorf("user with ID %s not found", userID) } newTodo := &model.Todo{ ID: randString(8), // Generate a unique ID Text: text, Done: false, User: user, } r.todos = append(r.todos, newTodo) return newTodo, nil } // MarkTodoDone is the resolver for the markTodoDone field. func (r *mutationResolver) MarkTodoDone(ctx context.Context, id string) (*model.Todo, error) { r.mu.Lock() defer r.mu.Unlock() for _, todo := range r.todos { if todo.ID == id { todo.Done = true return todo, nil } } return nil, fmt.Errorf("todo with ID %s not found", id) } // Todos is the resolver for the todos field. func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) { r.mu.Lock() defer r.mu.Unlock() return r.todos, nil } // User is the resolver for the user field. func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) { // The user is already embedded in the Todo model, so we just return it. // In a real application, you might fetch the user from a database here if it's not eager-loaded. return obj.User, nil } // Mutation returns MutationResolver implementation. func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } // Todo returns TodoResolver implementation. func (r *Resolver) Todo() TodoResolver { return &todoResolver{r} } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type todoResolver struct{ *Resolver }
Beachten Sie den generierten Todo()
-Resolver für das User
-Feld innerhalb von Todo
. Dies ist ein Feld-Resolver, der es Ihnen ermöglicht, anzupassen, wie ein bestimmtes Feld eines Typs aufgelöst wird. Da unser Todo
das User
-Objekt bereits enthält, geben wir es einfach zurück. Wenn der Benutzer nur als ID
in der Todo
-Struktur gespeichert wäre, würden wir hier das User
-Objekt aus unserem Datenspeicher basierend auf dieser ID abrufen. Diese Flexibilität ist eine Schlüsselstärke von GraphQL.
Einrichten des Servers
Schließlich müssen wir einen HTTP-Server einrichten, um unsere GraphQL-API verfügbar zu machen. Erstellen Sie eine Datei server.go
im Stammverzeichnis Ihres Projekts:
// server.go package main import ( "log" "net/http" "os" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "todo-graphql-server/graph" "todo-graphql-server/graph/model" ) const defaultPort = "8080" func main() { port := os.Getenv("PORT") if port == "" { port = defaultPort } // Initialisieren unseres Resolvers mit einigen Dummy-Daten resolver := &graph.Resolver{ todos: []*model.Todo{}, users: []*model.User{ {ID: "U1", Name: "Alice"}, {ID: "U2", Name: "Bob"}, }, } srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: resolver})) http.Handle("/", playground.Handler("GraphQL playground", "/query")) http.Handle("/query", srv) log.Printf("Connect to http://localhost:%s/ for GraphQL playground", port) log.Fatal(http.ListenAndServe(":"+port, nil)) }
In server.go
initialisieren wir unseren graph.Resolver
und übergeben ihn an das ausführbare Schema von gqlgen
. Wir richten dann zwei HTTP-Handler ein: einen für die GraphQL-Playground (eine nützliche GUI zum Testen Ihrer API) und einen anderen für den eigentlichen GraphQL-Endpunkt.
Ausführen und Testen des Servers
Führen Sie Ihren Server aus:
go run server.go
Öffnen Sie Ihren Browser unter http://localhost:8080/
. Sie sehen das GraphQL-Playground.
Führen wir einige Operationen durch:
Todo erstellen:
mutation CreateTodo { createTodo(text: "Learn gqlgen", userId: "U1") { id text done user { name } } }
Alle Todos abrufen:
query GetTodos { todos { id text done user { id name } } }
Ein Todo als erledigt markieren (ersetzen Sie TODO_ID
durch die ID aus der createTodo
-Mutation):
mutation MarkTodoDone { markTodoDone(id: "TODO_ID") { id text done user { name } } }
Sie werden feststellen, dass die Antworten stark typisiert sind und der Struktur Ihrer Abfragen entsprechen. gqlgen
stellt sicher, dass Ihre Go-Resolver-Funktionen Argumente erhalten und Werte zurückgeben, die genau mit Ihrem GraphQL-Schema übereinstimmen, und bietet somit eine hervorragende Typsicherheit während des gesamten Entwicklungsprozesses.
Anwendungsszenarien
Dieser typsichere Schema-First-Ansatz mit gqlgen
eignet sich ideal für:
- Große, kollaborative Teams: Das Schema fungiert als klarer Vertrag und hilft Frontend- und Backend-Teams, parallel zu arbeiten und Missverständnisse zu reduzieren.
- Komplexe APIs: Wenn Ihre API-Oberfläche wächst, helfen der generierte Code und die Typsicherheit, die Komplexität zu bewältigen und subtile Fehler zu vermeiden.
- Microservices-Architekturen: GraphQL kann als API-Gateway fungieren und Daten aus verschiedenen Microservices aggregieren.
gqlgen
erleichtert die Definition eines einheitlichen Schemas für dieses Gateway. - Öffentliche APIs: Ein gut definiertes und typsicheres Schema vereinfacht die Generierung von Client-Bibliotheken und verbessert die Entwicklererfahrung für API-Konsumenten.
Fazit
Der Aufbau eines typsicheren Schema-First GraphQL-Servers in Go mit gqlgen
bietet eine leistungsstarke Kombination aus Entwicklungseffizienz, robuster Typüberprüfung und wartbarem Code. Indem Sie mit Ihrem GraphQL-Schema als definitivem Vertrag beginnen, eliminiert gqlgen
Boilerplate-Code und stellt sicher, dass Ihre Go-Resolver präzise auf die Spezifikationen Ihrer API abgestimmt sind, was zu weniger Fehlern und einer reibungsloseren Entwicklung führt. Dieser Ansatz bietet eine solide Grundlage für jede sich entwickelnde API und überbrückt die Lücke zwischen flexibler Datenabfrage und inhärenter Go-Typsicherheit.