Protocolo gRPC-Connect: Comunicação unificada frontend-backend para microsserviços Go 2026
Introdução: Por que a comunicação frontend-backend dos seus microsserviços ainda é uma bagunça
Estamos em 2026. Se seus microsserviços Go ainda usam REST + JSON e mantêm manualmente dois conjuntos de definições de API (Protobuf do backend + TypeScript do frontend), você está afundado no seguinte pântano: o backend muda um campo e o frontend não fica sabendo, a documentação do Swagger está sempre desatualizada, a comunicação por streaming requer gambiarras com WebSocket, os códigos de erro são definidos separadamente em cada lado...
O protocolo gRPC-Connect acaba com tudo isso. Ele define um conjunto de serviços baseado em Protobuf, o backend usa gRPC para comunicação de alta performance e o frontend faz chamadas sem interrupções usando o modo HTTP/JSON do Connect-RPC. Um Proto, ambos os lados funcionam. A maturidade do ecossistema Buf torna isso mais simples do que nunca.
Este artigo guia você na construção de 5 padrões essenciais do gRPC-Connect em Go, cobrindo toda a cadeia da definição de serviços até o gateway pronto para produção.
Conceitos essenciais de um olhar
| Conceito | Descrição | Ferramentas do ecossistema |
|---|---|---|
| gRPC-Connect | Protocolo RPC baseado em HTTP/2 que suporta os protocolos gRPC e Connect | connect-go |
| Connect-RPC | Implementação Go do protocolo Connect, suportando modos gRPC/gRPC-Web/Connect | connectrpc.com |
| Buf | Toolchain de build Protobuf, substituindo protoc | buf.build |
| gRPC-Web | Protocolo gRPC do lado do navegador | connect-web |
| Protobuf | Linguagem de Definição de Interface | google.golang.org/protobuf |
| Streaming | Streaming de Servidor/Cliente/Bidirecional | connect-go |
| Interceptor | Mecanismo de middleware do Connect | connect-go |
Cinco pontos de dor: Por que a comunicação frontend-backend tradicional não consegue acompanhar
Ponto de dor 1: O pesadelo da manutenção de definições de API duplas. O backend define serviços gRPC com Protobuf, o frontend define APIs REST com interfaces TypeScript — dois conjuntos de definições que nunca estão sincronizados.
Ponto de dor 2: A documentação do Swagger é inútil. A documentação OpenAPI mantida manualmente sempre fica atrás do código real. Desenvolvedores frontend constantemente depuram contra documentação desatualizada.
Ponto de dor 3: Soluções de streaming fragmentadas. O Streaming gRPC não está disponível em navegadores, forçando a adoção de WebSocket e levando à fragmentação do protocolo.
Ponto de dor 4: Tratamento de erros inconsistente. gRPC usa Status Codes, REST usa HTTP Status Codes — o frontend precisa de dois conjuntos de lógica de tratamento de erros.
Ponto de dor 5: Toolchain de geração de código caótica. Conflitos de versão de plugins protoc, estilos de código gerado inconsistentes e integração CI/CD difícil.
Padrão 1: Definição de serviços Connect-RPC
Defina serviços usando Buf e Connect-RPC — um Proto gera tanto código de backend Go quanto código de frontend TypeScript.
// Runtime: Buf v1.47+, connect-go v1.18+, protoc-gen-go v1.34+
// File: proto/order/v1/order.proto
syntax = "proto3";
package order.v1;
option go_package = "github.com/example/gen/order/v1;orderv1";
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse) {}
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse) {}
rpc StreamOrders(StreamOrdersRequest) returns (stream Order) {}
rpc UploadOrders(stream UploadOrderRequest) returns (UploadOrdersResponse) {}
rpc OrderChat(stream ChatMessage) returns (stream ChatMessage) {}
}
message CreateOrderRequest {
string user_id = 1;
repeated OrderItem items = 2;
string shipping_address = 3;
}
message CreateOrderResponse {
string order_id = 1;
string status = 2;
int64 created_at = 3;
}
message GetOrderRequest {
string order_id = 1;
}
message GetOrderResponse {
Order order = 1;
}
message Order {
string order_id = 1;
string user_id = 2;
repeated OrderItem items = 3;
string status = 4;
int64 created_at = 5;
int64 updated_at = 6;
}
message OrderItem {
string product_id = 1;
string product_name = 2;
int32 quantity = 3;
double price = 4;
}
message StreamOrdersRequest {
string user_id = 1;
}
message UploadOrderRequest {
string user_id = 1;
repeated OrderItem items = 2;
}
message UploadOrdersResponse {
int32 total_created = 1;
repeated string order_ids = 2;
}
message ChatMessage {
string sender = 1;
string message = 2;
int64 timestamp = 3;
}
# File: buf.yaml
version: v2
modules:
- path: proto
name: buf.build/example/orders
lint:
use:
- STANDARD
breaking:
use:
- FILE
# File: buf.gen.yaml
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: github.com/example/gen
plugins:
- remote: buf.build/connectrpc/go:v1.18.0
out: gen/go
opt: paths=source_relative
- remote: buf.build/protocolbuffers/go:v1.34.0
out: gen/go
opt: paths=source_relative
- remote: buf.build/connectrpc/es:v2.0.0
out: gen/ts
- remote: buf.build/bufbuild/es:v2.0.0
out: gen/ts
// Runtime: Go 1.22+, connect-go v1.18.0
// File: server/main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"connectrpc.com/connect"
"connectrpc.com/grpcreflect"
"github.com/example/gen/order/v1/orderv1connect"
orderpb "github.com/example/gen/order/v1"
)
// OrderServiceHandler implements the order service
type OrderServiceHandler struct {
orders map[string]*orderpb.Order
}
// CreateOrder creates a new order
func (h *OrderServiceHandler) CreateOrder(
ctx context.Context,
req *connect.Request[orderpb.CreateOrderRequest],
) (*connect.Response[orderpb.CreateOrderResponse], error) {
msg := req.Msg
orderID := fmt.Sprintf("ord-%d", time.Now().UnixNano())
order := &orderpb.Order{
OrderId: orderID,
UserId: msg.UserId,
Items: msg.Items,
Status: "CREATED",
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
h.orders[orderID] = order
resp := connect.NewResponse(&orderpb.CreateOrderResponse{
OrderId: orderID,
Status: "CREATED",
CreatedAt: order.CreatedAt,
})
resp.Header().Set("X-Request-Id", fmt.Sprintf("req-%d", time.Now().UnixNano()))
return resp, nil
}
// GetOrder retrieves an order by ID
func (h *OrderServiceHandler) GetOrder(
ctx context.Context,
req *connect.Request[orderpb.GetOrderRequest],
) (*connect.Response[orderpb.GetOrderResponse], error) {
order, exists := h.orders[req.Msg.OrderId]
if !exists {
return nil, connect.NewError(connect.CodeNotFound,
fmt.Errorf("order %s not found", req.Msg.OrderId))
}
resp := connect.NewResponse(&orderpb.GetOrderResponse{
Order: order,
})
return resp, nil
}
// StreamOrders streams orders to the client
func (h *OrderServiceHandler) StreamOrders(
ctx context.Context,
req *connect.Request[orderpb.StreamOrdersRequest],
stream *connect.ServerStream[orderpb.Order],
) error {
for _, order := range h.orders {
if order.UserId == req.Msg.UserId {
if err := stream.Send(order); err != nil {
return fmt.Errorf("failed to stream order: %w", err)
}
time.Sleep(100 * time.Millisecond)
}
}
return nil
}
// UploadOrders handles client streaming upload
func (h *OrderServiceHandler) UploadOrders(
ctx context.Context,
stream *connect.ClientStream[orderpb.UploadOrderRequest],
) (*connect.Response[orderpb.UploadOrdersResponse], error) {
var totalCreated int32
var orderIDs []string
for stream.Receive() {
msg := stream.Msg()
orderID := fmt.Sprintf("ord-%d", time.Now().UnixNano())
order := &orderpb.Order{
OrderId: orderID,
UserId: msg.UserId,
Items: msg.Items,
Status: "CREATED",
CreatedAt: time.Now().Unix(),
}
h.orders[orderID] = order
totalCreated++
orderIDs = append(orderIDs, orderID)
}
if stream.Err() != nil {
return nil, connect.NewError(connect.CodeInternal, stream.Err())
}
resp := connect.NewResponse(&orderpb.UploadOrdersResponse{
TotalCreated: totalCreated,
OrderIds: orderIDs,
})
return resp, nil
}
// OrderChat handles bidirectional streaming chat
func (h *OrderServiceHandler) OrderChat(
ctx context.Context,
stream *connect.BidiStream[orderpb.ChatMessage, orderpb.ChatMessage],
) error {
for {
msg, err := stream.Receive()
if err != nil {
return nil
}
reply := &orderpb.ChatMessage{
Sender: "system",
Message: fmt.Sprintf("Received from %s: %s", msg.Sender, msg.Message),
Timestamp: time.Now().Unix(),
}
if err := stream.Send(reply); err != nil {
return fmt.Errorf("failed to send reply: %w", err)
}
}
}
func main() {
handler := &OrderServiceHandler{
orders: make(map[string]*orderpb.Order),
}
mux := http.NewServeMux()
path, orderHandler := orderv1connect.NewOrderServiceHandler(handler)
mux.Handle(path, orderHandler)
reflector := grpcreflect.NewStaticReflector(
orderv1connect.OrderServiceName,
)
mux.Handle(grpcreflect.NewHandlerV1(reflector))
mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector))
log.Println("Connect-RPC server starting on :8080")
log.Println("Supported protocols: gRPC, gRPC-Web, Connect")
log.Fatal(http.ListenAndServe(":8080", mux))
}
Padrão 2: Chamadas frontend gRPC-Web
Use Connect-Web para chamar serviços gRPC diretamente do frontend, eliminando a camada de middleware REST.
// Runtime: TypeScript 5.5+, @connectrpc/connect-web v2.0.0
// File: frontend/src/client.ts
import { createConnectTransport } from "@connectrpc/connect-web";
import { createClient } from "@connectrpc/connect";
import { OrderService } from "../gen/ts/order/v1/order_pb";
// Create Connect transport (HTTP/1.1 compatible)
const connectTransport = createConnectTransport({
baseUrl: "https://api.example.com",
});
// Create gRPC-Web transport
const grpcWebTransport = createConnectTransport({
baseUrl: "https://api.example.com",
httpVersion: "2",
});
// Create order service client
const orderClient = createClient(OrderService, connectTransport);
// === Unary call ===
async function createOrder() {
try {
const response = await orderClient.createOrder({
userId: "user-123",
items: [
{ productId: "prod-1", productName: "Go Programming", quantity: 1, price: 89.0 },
{ productId: "prod-2", productName: "Rust in Action", quantity: 2, price: 99.0 },
],
shippingAddress: "123 Main St, San Francisco",
});
console.log("Order created:", response.orderId, response.status);
} catch (err) {
console.error("Failed to create order:", err.code, err.message);
}
}
// === Get order ===
async function getOrder(orderId: string) {
try {
const response = await orderClient.getOrder({ orderId });
console.log("Order details:", response.order);
} catch (err: any) {
if (err.code === "NOT_FOUND") {
console.warn("Order not found");
} else {
console.error("Failed to get order:", err.message);
}
}
}
// === Server Streaming ===
async function streamOrders(userId: string) {
try {
for await (const order of orderClient.streamOrders({ userId })) {
console.log("Real-time order update:", order.orderId, order.status);
updateOrderInUI(order);
}
} catch (err) {
console.error("Order stream interrupted:", err.message);
}
}
// === Client Streaming ===
async function uploadOrders() {
const orders = [
{ userId: "user-1", items: [{ productId: "p1", productName: "Item 1", quantity: 1, price: 10 }] },
{ userId: "user-2", items: [{ productId: "p2", productName: "Item 2", quantity: 2, price: 20 }] },
];
try {
const response = await orderClient.uploadOrders(orders);
console.log(`Successfully uploaded ${response.totalCreated} orders`);
} catch (err) {
console.error("Failed to upload orders:", err.message);
}
}
function useOrderService() {
return { createOrder, getOrder, streamOrders, uploadOrders };
}
function updateOrderInUI(order: any) {}
Padrão 3: Tratamento de erros e retentativas
O Connect-RPC fornece um mecanismo unificado de tratamento de erros — tanto o frontend quanto o backend usam os mesmos códigos de erro e formato de mensagem.
// Runtime: Go 1.22+, connect-go v1.18.0
// File: server/errors.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"connectrpc.com/connect"
)
// OrderError represents a business error
type OrderError struct {
Code connect.Code `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
Retryable bool `json:"retryable"`
}
func (e *OrderError) Error() string {
return fmt.Sprintf("[%s] %s: %s", e.Code, e.Message, e.Detail)
}
// ToConnectError converts to a Connect error
func (e *OrderError) ToConnectError() *connect.Error {
err := connect.NewError(e.Code, fmt.Errorf("%s: %s", e.Message, e.Detail))
if e.Retryable {
err.Meta().Set("Retry-After", "5")
err.Meta().Set("X-Retryable", "true")
}
return err
}
var (
ErrOrderNotFound = &OrderError{
Code: connect.CodeNotFound,
Message: "Order not found",
Retryable: false,
}
ErrOrderAlreadyCancelled = &OrderError{
Code: connect.CodeFailedPrecondition,
Message: "Order already cancelled",
Retryable: false,
}
ErrInsufficientStock = &OrderError{
Code: connect.CodeResourceExhausted,
Message: "Insufficient stock",
Retryable: true,
}
ErrPaymentTimeout = &OrderError{
Code: connect.CodeDeadlineExceeded,
Message: "Payment timeout",
Retryable: true,
}
)
// RetryInterceptor implements client-side retry logic
type RetryInterceptor struct {
maxRetries int
initialDelay time.Duration
maxDelay time.Duration
retryableCodes map[connect.Code]bool
}
func NewRetryInterceptor() *RetryInterceptor {
return &RetryInterceptor{
maxRetries: 3,
initialDelay: 100 * time.Millisecond,
maxDelay: 5 * time.Second,
retryableCodes: map[connect.Code]bool{
connect.CodeUnavailable: true,
connect.CodeResourceExhausted: true,
connect.CodeDeadlineExceeded: true,
connect.CodeAborted: true,
},
}
}
func (i *RetryInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
var lastErr error
for attempt := 0; attempt <= i.maxRetries; attempt++ {
resp, err := next(ctx, req)
if err == nil {
return resp, nil
}
connectErr, ok := err.(*connect.Error)
if !ok {
return nil, err
}
if !i.retryableCodes[connectErr.Code()] {
return nil, err
}
retryAfter := connectErr.Meta().Get("Retry-After")
delay := i.calculateDelay(attempt, retryAfter)
lastErr = err
log.Printf("Retry %d/%d, delay %v, error: %v",
attempt+1, i.maxRetries, delay, connectErr.Message())
select {
case <-time.After(delay):
case <-ctx.Done():
return nil, ctx.Err()
}
}
return nil, lastErr
}
}
func (i *RetryInterceptor) calculateDelay(attempt int, retryAfter string) time.Duration {
if retryAfter != "" {
if d, err := time.ParseDuration(retryAfter + "s"); err == nil {
return d
}
}
delay := i.initialDelay * time.Duration(1<<uint(attempt))
if delay > i.maxDelay {
delay = i.maxDelay
}
return delay
}
// RecoveryInterceptor recovers from panics in handlers
func RecoveryInterceptor() connect.UnaryInterceptorFunc {
return func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (resp connect.AnyResponse, err error) {
defer func() {
if r := recover(); r != nil {
log.Printf("[PANIC] Handler panic: %v", r)
err = connect.NewError(connect.CodeInternal,
fmt.Errorf("internal server error, please retry later"))
}
}()
return next(ctx, req)
}
}
}
// ErrorLoggingInterceptor logs errors
func ErrorLoggingInterceptor() connect.UnaryInterceptorFunc {
return func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
startTime := time.Now()
resp, err := next(ctx, req)
if err != nil {
connectErr, ok := err.(*connect.Error)
if ok {
log.Printf("[ERROR] method=%s code=%s msg=%s duration=%v",
req.Spec().Procedure,
connectErr.Code(),
connectErr.Message(),
time.Since(startTime),
)
}
}
return resp, err
}
}
}
func main() {
mux := http.NewServeMux()
interceptors := []connect.Interceptor{
RecoveryInterceptor(),
ErrorLoggingInterceptor(),
}
_ = interceptors
log.Println("Error handling service starting on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
Padrão 4: Comunicação por streaming
O Connect-RPC suporta totalmente três modos de comunicação por streaming com definições Protobuf unificadas para frontend e backend.
// Runtime: Go 1.22+, connect-go v1.18.0
// File: server/streaming.go
package main
import (
"context"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"sync"
"time"
"connectrpc.com/connect"
)
// OrderStatusStream manages order status subscriptions
type OrderStatusStream struct {
subscribers map[string]chan *OrderStatusUpdate
mu sync.RWMutex
}
type OrderStatusUpdate struct {
OrderID string `json:"order_id"`
Status string `json:"status"`
Message string `json:"message"`
}
func NewOrderStatusStream() *OrderStatusStream {
return &OrderStatusStream{
subscribers: make(map[string]chan *OrderStatusUpdate),
}
}
func (s *OrderStatusStream) Subscribe(userID string) <-chan *OrderStatusUpdate {
s.mu.Lock()
defer s.mu.Unlock()
ch := make(chan *OrderStatusUpdate, 100)
s.subscribers[userID] = ch
return ch
}
func (s *OrderStatusStream) Unsubscribe(userID string) {
s.mu.Lock()
defer s.mu.Unlock()
if ch, ok := s.subscribers[userID]; ok {
close(ch)
delete(s.subscribers, userID)
}
}
func (s *OrderStatusStream) Publish(update *OrderStatusUpdate) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, ch := range s.subscribers {
select {
case ch <- update:
default:
log.Printf("Subscriber channel full, dropping update: %s", update.OrderID)
}
}
}
// BatchOrderImporter handles batch order imports
type BatchOrderImporter struct {
processedCount int
failedCount int
mu sync.Mutex
}
type OrderImportRequest struct {
UserID string `json:"user_id"`
Data string `json:"data"`
}
type BatchImportResult struct {
BatchID string `json:"batch_id"`
ProcessedCount int32 `json:"processed_count"`
FailedCount int32 `json:"failed_count"`
}
func (b *BatchOrderImporter) ProcessStream(
ctx context.Context,
stream *connect.ClientStream[OrderImportRequest],
) (*BatchImportResult, error) {
batchID := fmt.Sprintf("batch-%d", time.Now().UnixNano())
for stream.Receive() {
req := stream.Msg()
if err := b.processOne(ctx, req); err != nil {
b.mu.Lock()
b.failedCount++
b.mu.Unlock()
continue
}
b.mu.Lock()
b.processedCount++
b.mu.Unlock()
}
if stream.Err() != nil {
return nil, connect.NewError(connect.CodeInternal, stream.Err())
}
return &BatchImportResult{
BatchID: batchID,
ProcessedCount: int32(b.processedCount),
FailedCount: int32(b.failedCount),
}, nil
}
func (b *BatchOrderImporter) processOne(ctx context.Context, req *OrderImportRequest) error {
select {
case <-time.After(time.Duration(rand.Intn(50)) * time.Millisecond):
case <-ctx.Done():
return ctx.Err()
}
return nil
}
// CollaborationRoom manages real-time collaboration
type CollaborationRoom struct {
clients map[string]chan *CollabMessage
mu sync.RWMutex
}
type CollabMessage struct {
UserID string `json:"user_id"`
Content string `json:"content"`
Type string `json:"type"`
}
func (r *CollaborationRoom) Join(userID string) chan *CollabMessage {
r.mu.Lock()
defer r.mu.Unlock()
ch := make(chan *CollabMessage, 50)
r.clients[userID] = ch
return ch
}
func (r *CollaborationRoom) Leave(userID string) {
r.mu.Lock()
defer r.mu.Unlock()
if ch, ok := r.clients[userID]; ok {
close(ch)
delete(r.clients, userID)
}
}
func (r *CollaborationRoom) Broadcast(msg *CollabMessage, excludeUserID string) {
r.mu.RLock()
defer r.mu.RUnlock()
for userID, ch := range r.clients {
if userID == excludeUserID {
continue
}
select {
case ch <- msg:
default:
log.Printf("Client %s channel full", userID)
}
}
}
func (r *CollaborationRoom) HandleCollabStream(
ctx context.Context,
stream *connect.BidiStream[CollabMessage, CollabMessage],
) error {
userID := stream.RequestHeader().Get("X-User-Id")
if userID == "" {
userID = fmt.Sprintf("user-%d", rand.Int63())
}
recvCh := r.Join(userID)
defer r.Leave(userID)
errCh := make(chan error, 1)
go func() {
for {
msg, err := stream.Receive()
if err != nil {
if err == io.EOF {
errCh <- nil
return
}
errCh <- err
return
}
r.Broadcast(msg, userID)
}
}()
for {
select {
case msg := <-recvCh:
if err := stream.Send(msg); err != nil {
return fmt.Errorf("failed to send collab message: %w", err)
}
case err := <-errCh:
return err
case <-ctx.Done():
return ctx.Err()
}
}
}
func main() {
room := &CollaborationRoom{
clients: make(map[string]chan *CollabMessage),
}
statusStream := NewOrderStatusStream()
_ = room
_ = statusStream
log.Println("Streaming service starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Padrão 5: Gateway Connect pronto para produção
Construa um gateway Connect-RPC pronto para produção com autenticação, limitação de taxa, monitoramento e desligamento graceful.
// Runtime: Go 1.22+, connect-go v1.18.0
// File: server/gateway.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"sync/atomic"
"syscall"
"time"
"connectrpc.com/connect"
"connectrpc.com/grpcreflect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func AuthInterceptor(jwtSecret string) connect.UnaryInterceptorFunc {
return func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
if strings.HasSuffix(req.Spec().Procedure, "/Health/Check") {
return next(ctx, req)
}
token := req.Header().Get("Authorization")
if token == "" {
return nil, connect.NewError(connect.CodeUnauthenticated,
fmt.Errorf("missing authentication token"))
}
token = strings.TrimPrefix(token, "Bearer ")
claims, err := validateJWT(token, jwtSecret)
if err != nil {
return nil, connect.NewError(connect.CodeUnauthenticated,
fmt.Errorf("invalid authentication token: %w", err))
}
ctx = context.WithValue(ctx, "userID", claims.UserID)
ctx = context.WithValue(ctx, "scopes", claims.Scopes)
return next(ctx, req)
}
}
}
type JWTClaims struct {
UserID string `json:"user_id"`
Scopes []string `json:"scopes"`
}
func validateJWT(token, secret string) (*JWTClaims, error) {
return &JWTClaims{
UserID: "user-from-token",
Scopes: []string{"orders:read", "orders:write"},
}, nil
}
type RateLimiter struct {
tokens atomic.Int64
maxTokens int64
refillRate time.Duration
}
func NewRateLimiter(maxTokens int64, refillRate time.Duration) *RateLimiter {
rl := &RateLimiter{
maxTokens: maxTokens,
refillRate: refillRate,
}
rl.tokens.Store(maxTokens)
go func() {
ticker := time.NewTicker(refillRate)
defer ticker.Stop()
for range ticker.C {
current := rl.tokens.Load()
if current < maxTokens {
rl.tokens.CompareAndSwap(current, current+1)
}
}
}()
return rl
}
func (rl *RateLimiter) Allow() bool {
for {
current := rl.tokens.Load()
if current <= 0 {
return false
}
if rl.tokens.CompareAndSwap(current, current-1) {
return true
}
}
}
func RateLimitInterceptor(limiter *RateLimiter) connect.UnaryInterceptorFunc {
return func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
if !limiter.Allow() {
return nil, connect.NewError(connect.CodeResourceExhausted,
fmt.Errorf("rate limit exceeded, please retry later"))
}
return next(ctx, req)
}
}
}
func TracingInterceptor() connect.UnaryInterceptorFunc {
return func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
startTime := time.Now()
traceID := req.Header().Get("X-Trace-Id")
if traceID == "" {
traceID = fmt.Sprintf("trace-%d", startTime.UnixNano())
}
log.Printf("[TRACE] id=%s method=%s start=%v",
traceID, req.Spec().Procedure, startTime)
resp, err := next(ctx, req)
duration := time.Since(startTime)
status := "OK"
if err != nil {
status = "ERROR"
}
log.Printf("[TRACE] id=%s method=%s status=%s duration=%v",
traceID, req.Spec().Procedure, status, duration)
if resp != nil {
resp.Header().Set("X-Trace-Id", traceID)
resp.Header().Set("X-Response-Time", duration.String())
}
return resp, err
}
}
}
type ConnectGateway struct {
server *http.Server
limiter *RateLimiter
shutdownCh chan os.Signal
}
func NewConnectGateway(addr string) *ConnectGateway {
gw := &ConnectGateway{
limiter: NewRateLimiter(1000, time.Second),
shutdownCh: make(chan os.Signal, 1),
}
mux := http.NewServeMux()
reflector := grpcreflect.NewStaticReflector()
mux.Handle(grpcreflect.NewHandlerV1(reflector))
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
interceptors := []connect.Interceptor{
TracingInterceptor(),
RateLimitInterceptor(gw.limiter),
AuthInterceptor("your-jwt-secret"),
}
_ = interceptors
gw.server = &http.Server{
Addr: addr,
Handler: h2c.NewHandler(mux, &http2.Server{}),
}
return gw
}
func (gw *ConnectGateway) Start() error {
signal.Notify(gw.shutdownCh, syscall.SIGINT, syscall.SIGTERM)
errCh := make(chan error, 1)
go func() {
log.Printf("Connect gateway starting on %s", gw.server.Addr)
log.Println("Supported protocols: gRPC, gRPC-Web, Connect (HTTP/1.1 & HTTP/2)")
if err := gw.server.ListenAndServe(); err != http.ErrServerClosed {
errCh <- err
}
}()
select {
case err := <-errCh:
return fmt.Errorf("gateway failed to start: %w", err)
case sig := <-gw.shutdownCh:
log.Printf("Received signal %v, starting graceful shutdown...", sig)
return gw.Shutdown()
}
}
func (gw *ConnectGateway) Shutdown() error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Println("Stopping new request acceptance...")
if err := gw.server.Shutdown(ctx); err != nil {
return fmt.Errorf("gateway shutdown failed: %w", err)
}
log.Println("Gateway gracefully shut down")
return nil
}
func main() {
gateway := NewConnectGateway(":8080")
if err := gateway.Start(); err != nil {
log.Fatalf("Gateway failed: %v", err)
}
}
Guia de armadilhas: 5 armadilhas de nível produção
Armadilha 1: Conflitos de versão na geração de código do Buf. Diferentes versões de plugins geram código incompatível, causando falhas de compilação. Solução: Bloquear versões de plugins no buf.gen.yaml, usar buf generate no CI em vez de protoc local.
Armadilha 2: Falha na negociação HTTP/2. Alguns proxies reversos (versões antigas do Nginx) não suportam HTTP/2, causando falhas em chamadas gRPC. Solução: Usar o protocolo Connect (compatível com HTTP/1.1) como fallback, ou atualizar proxies para suportar h2c.
Armadilha 3: Desconexão de stream passa despercebida. Instabilidade de rede causa quedas em conexões de streaming sem que o servidor saiba. Solução: Implementar mecanismos de heartbeat (ping a cada 30 segundos), definir timeouts de leitura/escrita e auto-reconexão do cliente.
Armadilha 4: Corpos de mensagens grandes causam OOM. Upload de arquivos grandes por streaming enche o buffer do receptor, causando OOM. Solução: Definir tamanho máximo do corpo da mensagem (o padrão do Connect é 4MB), processar em chunks de streaming, implementar backpressure.
Armadilha 5: Configuração CORS ausente. Chamadas Connect do lado do navegador são bloqueadas pela política CORS. Solução: Configurar middleware CORS no nível do gateway, permitindo Content-Type: application/proto e application/json.
Referência rápida de solução de erros
| Mensagem de erro | Causa | Solução |
|---|---|---|
connect: code = Unauthenticated |
Token de autenticação ausente ou inválido | Verificar header Authorization e validade do JWT |
connect: code = NotFound |
Recurso solicitado não existe | Verificar parâmetros da requisição e ID do recurso |
connect: code = ResourceExhausted |
Limite de taxa de requisições excedido | Reduzir frequência de requisições ou ajustar configuração de limite |
connect: code = Unavailable |
Serviço indisponível | Verificar saúde do serviço e conectividade de rede |
http2: frame too large |
Corpo da mensagem excede o limite padrão | Ajustar a opção connect.MaxRecvMsgSize |
CORS policy: No Access-Control-Allow-Origin |
Configuração CORS ausente | Adicionar middleware CORS no gateway |
proto: invalid wire format |
Incompatibilidade de codificação/decodificação Protobuf | Verificar consistência de versão do arquivo Proto |
buf: plugin not found |
Plugin do Buf não instalado | Executar buf generate com plugins remotos |
stream recv: context canceled |
Cliente cancelou a requisição ou excedeu o tempo | Aumentar timeout ou verificar lógica do cliente |
tls: handshake failure |
Configuração TLS incorreta | Verificar configuração de certificados ou usar h2c para desenvolvimento |
Otimização avançada: 5 dicas de nível produção
Dica 1: Gerenciamento de versões de arquivos Proto. Usar o BSR (Buf Schema Registry) do Buf para gerenciar versões de arquivos Proto, habilitando versionamento de API e verificações de compatibilidade retroativa.
Dica 2: Estratégia de fallback do protocolo Connect. Seleção automática de protocolo com base na capacidade do cliente: gRPC (backend-para-backend), gRPC-Web (navegadores antigos), Connect (navegadores modernos) — sem necessidade de troca manual.
Dica 3: Controle de backpressure em streams. Implementar backpressure no streaming do servidor ajustando dinamicamente a taxa de push com base na velocidade de consumo do cliente, prevenindo overflow do buffer.
Dica 4: Agrupamento de requisições. Usar o Client Streaming do Connect para mesclar múltiplas requisições pequenas em uma requisição de streaming, reduzindo viagens de ida e volta na rede.
Dica 5: Integração de observabilidade. Integrar OpenTelemetry nos interceptors para gerar automaticamente Spans e Métricas, incorporando cadeias de chamadas Connect no rastreamento distribuído completo.
Análise comparativa
| Dimensão | REST + JSON | gRPC | gRPC-Connect |
|---|---|---|---|
| Protocolo | HTTP/1.1 | HTTP/2 | HTTP/1.1 + HTTP/2 |
| Formato de dados | JSON | Protobuf | Protobuf + JSON |
| Suporte a navegador | Nativo | Requer gRPC-Web | Nativo (protocolo Connect) |
| Streaming | WebSocket | Nativo | Nativo |
| Geração de código | OpenAPI/Swagger | protoc | Buf |
| Segurança de tipos | Fraca (JSON) | Forte (Protobuf) | Forte (Protobuf) |
| Tratamento de erros | Código de status HTTP | Status gRPC | Status Connect (compatível com gRPC) |
| Curva de aprendizado | Baixa | Média | Média |
| Performance | Média | Alta | Alta |
| Unificação frontend-backend | Não | Não | Sim |
Conclusão
Os 5 padrões essenciais do protocolo gRPC-Connect resolvem os pontos de dor chave da comunicação frontend-backend de microsserviços: a definição de serviços Connect-RPC permite um Proto para ambos os lados, as chamadas frontend gRPC-Web eliminam a camada de middleware REST, o tratamento unificado de erros e mecanismos de retentativa garantem confiabilidade, a comunicação por streaming fornece capacidades completas de comunicação em tempo real e o gateway Connect pronto para produção garante a estabilidade do sistema.
gRPC-Connect não é um substituto do REST — é a melhor prática para comunicação frontend-backend unificada. Se você está construindo um sistema de microsserviços Go, o ecossistema gRPC-Connect + Buf é a stack tecnológica na qual vale a pena investir em 2026. Lembre-se: uma definição, ambos os lados funcionam — esse é o valor central do gRPC-Connect.
Ferramentas online recomendadas
- /en/json/format — Formatador JSON para visualizar requisições e respostas JSON do protocolo Connect
- /en/dev/curl-to-code — Conversor de cURL para código para gerar rapidamente código de chamada de cliente Connect
- /en/encode/hash — Calculadora de hash para verificar resumos de mensagens Protobuf
- /en/text/diff — Ferramenta de diff de texto para comparar mudanças em arquivos Proto entre versões
Experimente estas ferramentas executadas localmente no navegador — nenhum cadastro necessário →