# CLAUDE.md — gRPC en Go : streaming temps réel pour microservices > Contexte spécialisé pour Claude Code. Coller ce fichier à la racine du projet pour implémenter du streaming gRPC entre services Go (server streaming, client streaming, bidirectionnel). --- ## Section 1 : Setup Protobuf et génération de code Tout commence par le fichier `.proto` — source de vérité partagée entre serveur et clients. Ne jamais écrire les structs Go à la main : elles sont générées par `protoc`. ```protobuf syntax = "proto3"; package pricefeed; option go_package = "./pb"; message PriceUpdate { string pair = 1; // "BTC/USDT" string exchange = 2; // "binance" double bid = 3; double ask = 4; int64 timestamp = 5; // unix millis } message SubscribeRequest { repeated string pairs = 1; // ["BTC/USDT", "ETH/USDT"] } service PriceFeed { // Server streaming : le client s'abonne, le serveur pousse rpc Subscribe(SubscribeRequest) returns (stream PriceUpdate); // Unary : dernier prix connu rpc GetLatest(SubscribeRequest) returns (PriceUpdate); } ``` Génération du code Go : ```bash protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ pricefeed.proto ``` Résultat : deux fichiers dans `./pb/` — structs et interfaces serveur/client. **Ne jamais modifier ces fichiers à la main** : ils seront écrasés à la prochaine génération. --- ## Section 2 : Les 4 modes de streaming Choisir le bon mode selon le pattern de communication. ### Unary — requête / réponse classique ```protobuf rpc GetLatest(SubscribeRequest) returns (PriceUpdate); ``` Comportement REST classique. Idéal pour lectures ponctuelles. Pas de `stream` dans la signature. ### Server streaming — abonnement à un flux ```protobuf rpc Subscribe(SubscribeRequest) returns (stream PriceUpdate); ``` Le client envoie **une** requête, le serveur pousse autant de réponses qu'il veut. Parfait pour les feeds : prix, métriques, logs. ### Client streaming — envoi de données en batch ```protobuf rpc BatchOrders(stream Order) returns (BatchResult); ``` Le client envoie un flux de requêtes, le serveur répond **une seule fois** à la fin. Cas d'usage : ingestion en bulk, flux d'ordres à agréger. ### Bidirectionnel — flux dans les deux sens ```protobuf rpc MarketDataFeed(stream MarketQuery) returns (stream MarketEvent); ``` Le mode le plus complexe à implémenter et déboguer. Pour la plupart des besoins microservices, server streaming ou unary suffisent. Le bidirectionnel est souvent sur-ingénié. --- ## Section 3 : Implémentation serveur Go Le serveur implémente l'interface générée par `protoc`. Toujours embedder `pb.UnimplementedXxxServer` pour la compatibilité forward. ```go type PriceFeedServer struct { pb.UnimplementedPriceFeedServer updates chan *pb.PriceUpdate } func (s *PriceFeedServer) Subscribe(req *pb.SubscribeRequest, stream pb.PriceFeed_SubscribeServer) error { pairs := make(map[string]bool) for _, p := range req.Pairs { pairs[p] = true } for { select { case update := <-s.updates: if !pairs[update.Pair] { continue } if err := stream.Send(update); err != nil { // Client déconnecté — pas une erreur serveur return nil } case <-stream.Context().Done(): return nil // Client a annulé la souscription } } } ``` **Points critiques :** - `stream.Send()` retourne une erreur si le client est déconnecté. Retourner `nil` est intentionnel : déconnexion normale, pas une erreur serveur. - `stream.Context().Done()` catch les cancellations et timeouts. Sans ça, la goroutine continue après déconnexion — goroutine leak. - Le channel `updates` est alimenté par la goroutine qui collecte les données. Le serveur gRPC ne fait que distribuer. Démarrage du serveur : ```go func main() { updates := make(chan *pb.PriceUpdate, 100) srv := grpc.NewServer() pb.RegisterPriceFeedServer(srv, &PriceFeedServer{updates: updates}) lis, err := net.Listen("tcp", ":50051") if err != nil { slog.Error("failed to listen", "error", err) os.Exit(1) } slog.Info("gRPC server listening", "addr", ":50051") if err := srv.Serve(lis); err != nil { slog.Error("serve error", "error", err) os.Exit(1) } } ``` --- ## Section 4 : Implémentation client Go La connexion gRPC est réutilisable — la créer une fois et l'injecter dans les services qui en ont besoin. ```go func connectPriceFeed(ctx context.Context, addr string) error { conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return fmt.Errorf("dial: %w", err) } defer conn.Close() client := pb.NewPriceFeedClient(conn) stream, err := client.Subscribe(ctx, &pb.SubscribeRequest{ Pairs: []string{"BTC/USDT", "ETH/USDT"}, }) if err != nil { return fmt.Errorf("subscribe: %w", err) } for { update, err := stream.Recv() if err == io.EOF { return nil // Serveur a fermé le stream proprement } if err != nil { return fmt.Errorf("recv: %w", err) } slog.Info("price update", "pair", update.Pair, "bid", update.Bid, "ask", update.Ask, "exchange", update.Exchange, ) } } ``` **Notes importantes :** - `insecure.NewCredentials()` : pour dev local et communications intra-cluster (mTLS géré par le service mesh). En production sur réseau public : utiliser `credentials.NewClientTLSFromFile()` ou `credentials.NewTLS()`. - La boucle `stream.Recv()` est bloquante sans consommer de CPU — HTTP/2 gère ça, pas de polling actif. - Quand le contexte est annulé, `Recv()` retourne une erreur et la boucle s'arrête proprement. --- ## Section 5 : Error handling gRPC gRPC a son propre système de codes d'erreur. Les mapper correctement évite les malentendus entre services. ```go import "google.golang.org/grpc/codes" import "google.golang.org/grpc/status" // Côté serveur : retourner des erreurs typées func (s *PriceFeedServer) GetLatest(ctx context.Context, req *pb.SubscribeRequest) (*pb.PriceUpdate, error) { if len(req.Pairs) == 0 { return nil, status.Error(codes.InvalidArgument, "at least one pair required") } update, ok := s.cache.Get(req.Pairs[0]) if !ok { return nil, status.Errorf(codes.NotFound, "no data for pair %s", req.Pairs[0]) } return update, nil } // Côté client : inspecter le code d'erreur update, err := client.GetLatest(ctx, req) if err != nil { st, ok := status.FromError(err) if ok { switch st.Code() { case codes.NotFound: // Pas d'erreur critique — la paire n'est pas encore disponible return nil case codes.Unavailable: // Serveur temporairement indisponible — retry return fmt.Errorf("server unavailable, retry later: %w", err) default: return fmt.Errorf("grpc error %s: %w", st.Code(), err) } } return fmt.Errorf("unexpected error: %w", err) } ``` **Codes fréquents :** - `codes.OK` — succès - `codes.InvalidArgument` — paramètre invalide (équivalent HTTP 400) - `codes.NotFound` — ressource absente (équivalent HTTP 404) - `codes.Unavailable` — service temporairement indisponible, retryable (équivalent HTTP 503) - `codes.DeadlineExceeded` — timeout de contexte dépassé --- ## Section 6 : Testing gRPC Tester les services gRPC avec `bufconn` — un listener in-memory qui évite les vraies connexions réseau. ```go import "google.golang.org/grpc/test/bufconn" const bufSize = 1024 * 1024 func setupTestServer(t *testing.T) (pb.PriceFeedClient, func()) { t.Helper() lis := bufconn.Listen(bufSize) srv := grpc.NewServer() updates := make(chan *pb.PriceUpdate, 10) pb.RegisterPriceFeedServer(srv, &PriceFeedServer{updates: updates}) go srv.Serve(lis) conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { t.Fatalf("dial bufconn: %v", err) } client := pb.NewPriceFeedClient(conn) cleanup := func() { conn.Close() srv.GracefulStop() } return client, cleanup } func TestSubscribe(t *testing.T) { client, cleanup := setupTestServer(t) defer cleanup() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() stream, err := client.Subscribe(ctx, &pb.SubscribeRequest{Pairs: []string{"BTC/USDT"}}) if err != nil { t.Fatalf("subscribe: %v", err) } // Injecter un update et vérifier la réception // ... } ``` --- ## Section 7 : Deployment et debugging Outils pour déboguer en production sans Postman : ```bash # Installer grpcurl go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest # Lister les services disponibles grpcurl -plaintext localhost:50051 list # Lister les méthodes d'un service grpcurl -plaintext localhost:50051 list pricefeed.PriceFeed # Appel unary grpcurl -plaintext -d '{"pairs": ["BTC/USDT"]}' localhost:50051 pricefeed.PriceFeed/GetLatest # Server streaming (ctrl+C pour arrêter) grpcurl -plaintext -d '{"pairs": ["BTC/USDT", "ETH/USDT"]}' localhost:50051 pricefeed.PriceFeed/Subscribe ``` Graceful shutdown : ```go srv := grpc.NewServer() // ... quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit slog.Info("shutting down gRPC server") srv.GracefulStop() // Attend que les streams en cours se terminent ``` **Quand choisir gRPC vs REST :** - gRPC : services internes Go, besoin de streaming, contrats stricts entre équipes - REST : API publiques, clients navigateur, clients tiers non-Go - Ne pas gRPCifier une API publique — `grpc-web` ajoute de la complexité sans bénéfice notable