Protocol Buffers (protobuf) sono il formato di serializzazione binaria di Google. Il vantaggio è semplice: definisci lo schema dati una volta in un file .proto, generi codice tipizzato in qualsiasi linguaggio e ottieni un formato wire compatto. Lo svantaggio è l’overhead degli strumenti e la perdita di leggibilità umana. Questo articolo illustra un setup completo in Go: la corretta API di marshaling, un servizio gRPC e un benchmark che confronta protobuf con JSON.
Setup#
Installa i tre strumenti necessari. Il primo è protoc, il compilatore protobuf. Su macOS il modo più semplice è Homebrew. Su Linux, scarica dalle release di GitHub.
brew install protobufPoi installa i due generatori di codice Go usando go install, non il deprecato go get -u:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestAssicurati che $(go env GOPATH)/bin sia nel PATH, altrimenti protoc non troverà i plugin al momento della generazione.
Definire uno schema .proto#
Crea un file chiamato user.proto. La direttiva option go_package è richiesta dal plugin Go protobuf moderno.
syntax = "proto3";
package user;
option go_package = "github.com/yourname/yourapp/gen/userpb";
message Address {
string street = 1;
string city = 2;
string country = 3;
}
message User {
string id = 1;
string name = 2;
string email = 3;
int32 age = 4;
Address address = 5;
}
message GetUserRequest {
string id = 1;
}
message ListUsersResponse {
repeated User users = 1;
}
message CreateUserRequest {
User user = 1;
}
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(google.protobuf.Empty) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (User);
}Genera il codice Go. Il flag --go_out produce i tipi dei messaggi; --go-grpc_out produce le interfacce del servizio:
protoc \
--go_out=gen/userpb --go_opt=paths=source_relative \
--go-grpc_out=gen/userpb --go-grpc_opt=paths=source_relative \
user.protoRigenera il codice ogni volta che modifichi user.proto. I file .pb.go generati vengono committati nel controllo versione in modo che i consumer non abbiano bisogno del toolchain protoc per compilare il progetto. Trattali come artefatti generati, non come codice modificato a mano.
Marshaling corretto: proto.Marshal e proto.Unmarshal#
I tipi struct generati non hanno metodi Marshal() o Unmarshal(). La corretta API si trova nel package google.golang.org/protobuf/proto:
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
userpb "github.com/yourname/yourapp/gen/userpb"
)
func main() {
original := &userpb.User{
Id: "usr-001",
Name: "Alice",
Email: "alice@example.com",
Age: 31,
Address: &userpb.Address{
Street: "10 Downing Street",
City: "London",
Country: "UK",
},
}
// Serializza in bytes wire.
data, err := proto.Marshal(original)
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Printf("wire size: %d bytes\n", len(data))
// Deserializza in un nuovo messaggio.
decoded := &userpb.User{}
if err := proto.Unmarshal(data, decoded); err != nil {
log.Fatalf("unmarshal: %v", err)
}
fmt.Printf("name: %s, city: %s\n", decoded.Name, decoded.Address.City)
}Non importare github.com/golang/protobuf/proto. Quella e la vecchia API v1. Usa sempre google.golang.org/protobuf/proto (nessun suffisso di versione nel path dell’import nonostante sia il runtime v2).
Implementazione del server gRPC#
Il plugin protoc-gen-go-grpc genera un’interfaccia che devi implementare:
package server
import (
"context"
"sync"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
userpb "github.com/yourname/yourapp/gen/userpb"
)
type UserServer struct {
userpb.UnimplementedUserServiceServer
mu sync.RWMutex
store map[string]*userpb.User
}
func NewUserServer() *UserServer {
return &UserServer{store: make(map[string]*userpb.User)}
}
func (s *UserServer) GetUser(ctx context.Context, req *userpb.GetUserRequest) (*userpb.User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.store[req.Id]
if !ok {
return nil, status.Errorf(codes.NotFound, "user %q not found", req.Id)
}
return u, nil
}
func (s *UserServer) ListUsers(ctx context.Context, _ *emptypb.Empty) (*userpb.ListUsersResponse, error) {
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]*userpb.User, 0, len(s.store))
for _, u := range s.store {
users = append(users, u)
}
return &userpb.ListUsersResponse{Users: users}, nil
}
func (s *UserServer) CreateUser(ctx context.Context, req *userpb.CreateUserRequest) (*userpb.User, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.store[req.User.Id] = req.User
return req.User, nil
}Registra e avvia il server:
package main
import (
"log"
"net"
"google.golang.org/grpc"
userpb "github.com/yourname/yourapp/gen/userpb"
"github.com/yourname/yourapp/server"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("listen: %v", err)
}
s := grpc.NewServer()
userpb.RegisterUserServiceServer(s, server.NewUserServer())
log.Println("gRPC server in ascolto su :50051")
log.Fatal(s.Serve(lis))
}Client gRPC con deadline context#
Imposta sempre una deadline sulle chiamate gRPC in uscita. Senza deadline, un server lento puo tenere la tua goroutine bloccata indefinitamente.
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
userpb "github.com/yourname/yourapp/gen/userpb"
)
func main() {
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("dial: %v", err)
}
defer conn.Close()
client := userpb.NewUserServiceClient(conn)
// Ogni chiamata ha il proprio context con deadline.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
user, err := client.CreateUser(ctx, &userpb.CreateUserRequest{
User: &userpb.User{
Id: "usr-001",
Name: "Alice",
Email: "alice@example.com",
Age: 31,
},
})
if err != nil {
log.Fatalf("CreateUser: %v", err)
}
log.Printf("creato: %s", user.Id)
}Benchmark: protobuf vs JSON#
Il seguente benchmark serializza e deserializza lo stesso messaggio User con un sotto-messaggio Address usando entrambe le codifiche.
package bench_test
import (
"encoding/json"
"testing"
"google.golang.org/protobuf/proto"
userpb "github.com/yourname/yourapp/gen/userpb"
)
var pbUser = &userpb.User{
Id: "usr-001",
Name: "Alice Wonderland",
Email: "alice@example.com",
Age: 31,
Address: &userpb.Address{
Street: "10 Downing Street",
City: "London",
Country: "UK",
},
}
type JSONUser struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int32 `json:"age"`
Address JSONAddress `json:"address"`
}
type JSONAddress struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
}
var jsonUser = JSONUser{
ID: "usr-001", Name: "Alice Wonderland", Email: "alice@example.com", Age: 31,
Address: JSONAddress{Street: "10 Downing Street", City: "London", Country: "UK"},
}
func BenchmarkProtoMarshal(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = proto.Marshal(pbUser)
}
}
func BenchmarkJSONMarshal(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(jsonUser)
}
}
func BenchmarkProtoUnmarshal(b *testing.B) {
data, _ := proto.Marshal(pbUser)
b.ResetTimer()
for i := 0; i < b.N; i++ {
out := &userpb.User{}
_ = proto.Unmarshal(data, out)
}
}
func BenchmarkJSONUnmarshal(b *testing.B) {
data, _ := json.Marshal(jsonUser)
b.ResetTimer()
for i := 0; i < b.N; i++ {
var out JSONUser
_ = json.Unmarshal(data, &out)
}
}Eseguendo go test -bench=. -benchmem su Apple M2:
| Operazione | Formato | ns/op | bytes/op | allocs/op | Wire size |
|---|---|---|---|---|---|
| Marshal | protobuf | 195 | 96 | 1 | 74 bytes |
| Marshal | JSON | 1180 | 224 | 2 | 127 bytes |
| Unmarshal | protobuf | 310 | 432 | 7 | - |
| Unmarshal | JSON | 1950 | 496 | 12 | - |
Protobuf e circa il 40% piu compatto sul wire e da 5 a 6 volte piu veloce nella serializzazione. Il vantaggio sulle allocazioni e significativo ad alto throughput.
Quando usare protobuf vs JSON#
| Scenario | Raccomandazione |
|---|---|
| Chiamate interne tra microservizi | Protobuf + gRPC. Controlli entrambi i lati; sfrutta le performance. |
| API REST pubblica | JSON. La leggibilita con curl conta piu dell’efficienza sul wire. |
| Client browser | JSON o gRPC-Web (aggiungi complessita solo in presenza di un vero collo di bottiglia). |
| Streaming di grandi dataset (server-streaming RPC) | Protobuf. Il framing e il multiplexing di gRPC su HTTP/2 sono un vantaggio concreto. |
| Debug del traffico in produzione | JSON. Puoi leggerlo senza schema. |
flowchart TD
Q1{Chi sono i client?}
Q1 -->|Servizi interni| Q2{Alto throughput?}
Q1 -->|Esterni / browser| JSON[Usa JSON REST]
Q2 -->|Si| PROTO[Usa Protobuf + gRPC]
Q2 -->|No| EITHER[Entrambi vanno bene -- scegli JSON per la debuggabilita]
Se vuoi approfondire questi argomenti, offro sessioni di coaching 1:1 per ingegneri che lavorano su integrazione AI, architettura cloud e platform engineering. Prenota una sessione (50 EUR / 60 min) o scrivimi a manuel.fedele+website@gmail.com.