gRPC services in Go zijn een van de populairste keuzes voor performante, typeveilige communicatie tussen microservices. Waar een REST API tekstuele JSON over HTTP/1.1 stuurt, gebruikt gRPC binaire Protocol Buffers over HTTP/2, kleiner, sneller en met een strikt contract dat je compiler afdwingt.
In deze gids leer je hoe je een gRPC service bouwt in Go, van het eerste .proto bestand tot productieklare servers met streaming, interceptors en graceful shutdown. We bouwen voort op eerdere onderwerpen zoals microservices in Go en HTTP servers bouwen in Go.
Wat is gRPC?
gRPC is een open-source RPC-framework, oorspronkelijk ontwikkeld door Google. Het combineert drie dingen: HTTP/2 als transport, Protocol Buffers (protobuf) als serialisatieformaat en code-generatie voor client en server in meerdere talen.
Het resultaat is een API waar je geen URL paths of HTTP methods meer bedenkt. Je definieert methods op een service, en gRPC zorgt voor de rest. De gegenereerde Go-code voelt als een gewone functie-aanroep, ook al gaat er netwerkverkeer overheen.
Voor uitgebreide achtergrond verwijs ik graag naar de officiële gRPC documentatie.
Waarom gRPC in plaats van REST?
REST blijft prima voor veel scenarios, zeker voor publieke API's. Maar voor service-to-service communicatie binnen je eigen infrastructuur biedt gRPC duidelijke voordelen.
- Performance: binaire payloads zijn vaak 3 tot 10 keer kleiner dan JSON.
- Type-veiligheid: een protobuf-definitie is een afdwingbaar contract. Bij elke wijziging krijg je compile errors, niet runtime verrassingen.
- Streaming: bi-directionele streams zijn eerste-klas burgers in gRPC.
- Code generatie: clients in Go, Python, Java of Rust genereer je in seconden.
Het nadeel: browsers kunnen niet native met gRPC praten, en debugging vereist tools zoals grpcurl in plaats van curl.
Tooling installeren
Je hebt drie binaries nodig: de protoc compiler en twee Go-plugins.
# protoc installeren (macOS)
brew install protobuf
# Of via Linux package manager
apt-get install -y protobuf-compiler
# Go plugins installeren
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Zorg dat $GOPATH/bin in je PATH staat, anders vindt protoc de plugins niet. Voor een modernere ervaring kun je ook buf overwegen, dat vervangt handmatige protoc aanroepen met een fatsoenlijke config.
Een .proto bestand schrijven
Een gRPC service begint altijd bij het contract. Maak een map proto/ en een bestand user.proto.
syntax = "proto3";
package user.v1;
option go_package = "example.com/app/gen/user/v1;userv1";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User);
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
}
message User {
string id = 1;
string email = 2;
string name = 3;
}
Een paar dingen om op te merken. De go_package optie vertelt protoc-gen-go waar de gegenereerde code landt. De versie in het pakket (user.v1) is een goede gewoonte, als je later breaking changes maakt, introduceer je user.v2 zonder de eerste kapot te maken.
Code genereren
Draai protoc vanuit de root van je project.
protoc \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/user.proto
Dit genereert twee bestanden: user.pb.go (de messages) en user_grpc.pb.go (de service interfaces). Deze bestanden check je normaal gesproken wél in, behandel ze als artefacten, niet als handgeschreven code.
Een gRPC server bouwen
Nu kun je de service implementeren. De gegenereerde code geeft je een UserServiceServer interface die je moet invullen.
package main
import (
"context"
"log/slog"
"net"
userv1 "example.com/app/gen/user/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type userServer struct {
userv1.UnimplementedUserServiceServer
}
func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
if req.GetId() == "" {
return nil, status.Error(codes.InvalidArgument, "id is required")
}
return &userv1.User{
Id: req.GetId(),
Email: "[email protected]",
Name: "Sander",
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
slog.Error("listen failed", "err", err)
return
}
s := grpc.NewServer()
userv1.RegisterUserServiceServer(s, &userServer{})
slog.Info("gRPC server listening", "addr", lis.Addr())
if err := s.Serve(lis); err != nil {
slog.Error("serve failed", "err", err)
}
}
De embedded UnimplementedUserServiceServer is belangrijk. Die zorgt dat je service voorwaarts compatibel blijft als er later methods aan het contract worden toegevoegd, oude builds retourneren dan Unimplemented in plaats van te crashen.
Voor de foutafhandeling gebruik je status.Error met een codes constante. Dat komt goed aan bij clients omdat het gestandaardiseerde gRPC status codes produceert. Zie ook ons artikel over functions en error handling voor de Go-basics.
Een client schrijven
De client is nog eenvoudiger. Je opent een connectie en roept methods aan alsof het lokale functies zijn.
package main
import (
"context"
"log/slog"
"time"
userv1 "example.com/app/gen/user/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
slog.Error("dial failed", "err", err)
return
}
defer conn.Close()
client := userv1.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
user, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: "42"})
if err != nil {
slog.Error("GetUser failed", "err", err)
return
}
slog.Info("got user", "name", user.GetName())
}
De context.WithTimeout is cruciaal. gRPC gebruikt deze om een deadline door te sturen naar de server, als de server de deadline niet haalt, krijgt de client een DeadlineExceeded error. Meer achtergrond vind je in context package diep uitgelegd.
Streaming RPCs
Een van de grootste verschillen met REST is dat gRPC vier soorten calls ondersteunt: unary (gewoon request/response), server-streaming, client-streaming en bi-directioneel.
De ListUsers method uit onze proto is server-streaming. Implementatie:
func (s *userServer) ListUsers(req *userv1.ListUsersRequest, stream userv1.UserService_ListUsersServer) error {
users := []*userv1.User{
{Id: "1", Name: "Alice"},
{Id: "2", Name: "Bob"},
}
for _, u := range users {
if err := stream.Send(u); err != nil {
return err
}
}
return nil
}
Een client leest de stream in een lus tot io.EOF. Dit past uitstekend bij use cases zoals live feeds, paginatie over grote datasets of log-tailing. Als je eerder channels hebt gebruikt, voelt het denkmodel bekend.
Interceptors: de middleware van gRPC
gRPC heeft geen middleware-keten zoals net/http, maar interceptors vervullen dezelfde rol. Ze lopen rond elke RPC en zijn perfect voor logging, auth, metrics en recovery.
func loggingUnary(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
start := time.Now()
resp, err := handler(ctx, req)
slog.Info("rpc handled",
"method", info.FullMethod,
"duration", time.Since(start),
"err", err,
)
return resp, err
}
s := grpc.NewServer(grpc.ChainUnaryInterceptor(loggingUnary))
Voor streaming heb je een aparte StreamServerInterceptor. In productie gebruik je meestal kant-en-klare interceptors uit go-grpc-middleware voor recovery, auth en observability. Voor uitgebreide patronen, zie middleware en routing en logging en observability.
TLS en authenticatie
Draai nooit productie-gRPC zonder TLS. Dit is de basis:
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil {
return err
}
s := grpc.NewServer(grpc.Creds(creds))
Voor authenticatie op RPC-niveau is een interceptor die een JWT of API key uit de metadata leest een beproefd patroon. Metadata is gRPC's equivalent van HTTP headers.
Graceful shutdown
Net als bij een HTTP server wil je dat lopende RPCs netjes afgehandeld worden bij een SIGTERM.
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
if err := s.Serve(lis); err != nil {
slog.Error("serve", "err", err)
}
}()
<-sigCh
slog.Info("shutting down")
s.GracefulStop()
GracefulStop wacht tot alle actieve RPCs klaar zijn, maar weigert nieuwe. Voor tips over het draaien in productie, zie production deployment van Go apps.
Testen met bufconn
Gewone integratietesten van gRPC services kun je razendsnel maken zonder echte sockets met bufconn.
lis := bufconn.Listen(1024 * 1024)
s := grpc.NewServer()
userv1.RegisterUserServiceServer(s, &userServer{})
go s.Serve(lis)
conn, _ := grpc.NewClient("passthrough://bufnet",
grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
Elke test krijgt een verse in-memory server. Meer over testaanpak staat in testing in Go.
Veelgemaakte valkuilen
- Oneindige message groottes: de default max message size is 4 MB. Voor grote payloads moet je dit aan beide kanten ophogen, anders krijg je cryptische
ResourceExhaustederrors. - Geen deadlines vanuit clients: zonder timeout blijft een hangende RPC voor altijd hangen. Altijd
context.WithTimeoutgebruiken. - Breaking changes in proto: veld-nummers nooit hergebruiken. Verwijderde velden reserveer je met
reserved 5;om per ongeluk conflicten te voorkomen. - Vergeten
UnimplementedXServerin te bedden: levert breaking compile errors op zodra iemand de proto uitbreidt.
Veelgestelde vragen
Wat is gRPC precies?
gRPC is een high-performance RPC framework van Google dat HTTP/2 en Protocol Buffers gebruikt. Het laat services efficiënt met elkaar communiceren via strikt getypeerde contracten, met ondersteuning voor streaming en meerdere talen.
Wanneer kies je gRPC boven REST?
Kies gRPC voor service-to-service communicatie binnen je infrastructuur waar performance, type-veiligheid en streaming belangrijk zijn. REST blijft handiger voor publieke API's of browserclients zonder extra gateway.
Heb ik protoc nodig om gRPC in Go te gebruiken?
Ja, je hebt de protoc compiler met de Go-plugins protoc-gen-go en protoc-gen-go-grpc nodig om .proto bestanden om te zetten naar Go-code. Alternatief gebruik je buf, een modernere wrapper rond protoc.
Kan gRPC samenwerken met browsers?
Niet direct, omdat browsers geen volledige HTTP/2 trailers ondersteunen. Je gebruikt gRPC-Web via een proxy zoals Envoy, of je zet een REST-gateway voor je gRPC service met grpc-gateway.
Hoe test je gRPC services in Go?
Gebruik bufconn uit google.golang.org/grpc/test/bufconn voor in-memory tests zonder echte netwerkverbinding. Zo test je je handlers snel en deterministisch, inclusief interceptors en streaming.
Conclusie
gRPC in Go geeft je een typeveilige, performante manier om services met elkaar te laten praten, met streaming, interceptors en uitstekende tooling. De leercurve zit vooral in de protobuf-workflow en de tooling eromheen, maar zodra die staat, is het ontwikkelen van nieuwe endpoints vaak sneller dan met REST.
Begin klein: één service, twee methods, TLS erop. Voeg dan observability, auth en streaming toe naarmate je ze echt nodig hebt. Voor schaalbare achtergrondverwerking rondom je gRPC services is ons artikel over worker pools en pipelines een logische volgende stap.