Khi các service cần gọi nhau, REST/JSON là mặc định quen thuộc. Nhưng ở quy mô lớn — hàng triệu lời gọi nội bộ mỗi giây — chi phí của JSON (văn bản dài dòng, parse chậm, không có hợp đồng chặt) trở nên đáng kể. gRPC là câu trả lời của Google: một khung RPC dựa trên protobuf (mã hóa nhị phân) và HTTP/2, với hợp đồng định nghĩa trước và mã sinh tự động. Go là ngôn ngữ hạng nhất của gRPC. Bài này dựng một unary service (kiểu gọi đơn giản nhất: một request, một response) từ đầu, và đo thật vì sao gRPC nhanh và gọn hơn REST/JSON.
Bước 1: định nghĩa hợp đồng bằng .proto
gRPC là contract-first: bạn định nghĩa service và message trong một file .proto, và đó là nguồn sự thật duy nhất. Sinh mã từ đó cho cả client lẫn server:
syntax = "proto3";
service MayTinh {
rpc Cong(YeuCau) returns (KetQua); // unary: 1 request -> 1 response
}
message YeuCau { int64 a = 1; int64 b = 2; }
message KetQua { int64 tong = 1; }
Mỗi trường có một số thứ tự (a = 1) — số này, không phải tên, là thứ đi trên dây. Chạy protoc với plugin Go sinh ra hai file: .pb.go (message + marshal) và _grpc.pb.go (interface client/server):
protoc --go_out=. --go-grpc_out=. tinhtoan.proto
Kết quả sinh ra các interface MayTinhClient và MayTinhServer — bạn không viết tay phần transport, marshal, hay stub.

Hình 1: .proto định nghĩa service MayTinh với RPC unary Cong. protoc sinh client + server. Server hiện thực interface (Cong trả tổng); client gọi c.Cong(ctx, req) như một hàm cục bộ — gRPC lo phần transport.
Bước 2 và 3: server và client
Server chỉ cần hiện thực interface sinh ra (nhúng UnimplementedMayTinhServer để tương thích tiến), đăng ký và phục vụ:
type server struct{ tinhtoan.UnimplementedMayTinhServer }
func (s *server) Cong(ctx context.Context, r *tinhtoan.YeuCau) (*tinhtoan.KetQua, error) {
return &tinhtoan.KetQua{Tong: r.A + r.B}, nil
}
gs := grpc.NewServer()
tinhtoan.RegisterMayTinhServer(gs, &server{})
gs.Serve(lis)
Client quay số tới server và gọi RPC như một hàm cục bộ — gRPC che giấu toàn bộ việc marshal, gửi qua HTTP/2, unmarshal:
c := tinhtoan.NewMayTinhClient(conn)
res, _ := c.Cong(ctx, &tinhtoan.YeuCau{A: 20, B: 22}) // qua mạng
Đo thật (Go 1.23, grpc 1.67): server + client chạy end-to-end qua cổng 50051 — Cong(20, 22) = 42, Cong(100, -30) = 70. Lời gọi đi qua TCP + HTTP/2 thật, nhưng ở tầng code trông y hệt gọi hàm.
Đo thật: protobuf nhỏ và nhanh hơn JSON
Đây là lý do kỹ thuật chọn gRPC. So cùng một message {a: 20, b: 22}:

Hình 2: Server + client gRPC chạy thật (Cong(20,22)=42). Wire size: protobuf 4 byte (08141016) so với JSON 15 byte ({"a":20,"b":22}) — nhỏ hơn ~3,75x. Marshal: protobuf 33,56 ns (4 B, 1 alloc) so với JSON 70,89 ns (32 B, 2 alloc) — nhanh hơn ~2,1x.
- Kích thước dây: protobuf 4 byte (
08141016) so với JSON 15 byte ({"a":20,"b":22}) — nhỏ hơn ~3,75x. - Tốc độ marshal: protobuf 33,56 ns/op (4 B, 1 cấp phát) so với JSON 70,89 ns/op (32 B, 2 cấp phát) — nhanh hơn ~2,1x.
protobuf nhỏ hơn vì nó mã hóa theo số thứ tự trường (varint) và bỏ hoàn toàn tên trường — tên nằm trong .proto, không đi trên dây. JSON phải mang cả tên trường, dấu ngoặc, dấu nháy trong mỗi message. Ở quy mô hàng triệu message, chênh lệch này là băng thông và CPU thật.
Đánh đổi cần cân nhắc
gRPC cần bước sinh mã và tooling nặng hơn REST. Bạn phải cài protoc, các plugin Go, và chạy sinh mã mỗi khi đổi .proto. So với REST (chỉ cần encoding/json có sẵn), đây là ma sát thật lúc bắt đầu. gRPC trả cổ tức khi bạn có nhiều service gọi nhau và cần hợp đồng chặt; cho một API công khai đơn giản, REST/JSON thường đủ và dễ hơn.
protobuf nhị phân khó debug bằng mắt. JSON đọc được trực tiếp trong log, curl, trình duyệt; protobuf là byte thô cần công cụ giải mã. Khi gỡ lỗi giao tiếp, bạn mất khả năng "nhìn là hiểu" của JSON. Có grpcurl và reflection để bù, nhưng vẫn thêm một lớp.
gRPC cần HTTP/2 và không chạy thẳng trên trình duyệt. Trình duyệt không cho JavaScript kiểm soát HTTP/2 đủ mức để nói gRPC trực tiếp — cần một lớp proxy grpc-web. Với API mà client là trình duyệt, đây là phức tạp thêm; gRPC hợp nhất cho giao tiếp service-to-service phía sau, nơi cả hai đầu bạn kiểm soát.
Ba ý mang về
- gRPC là contract-first: định nghĩa service và message trong
.proto(mỗi trường có số thứ tự),protocsinh ra interface client và server — bạn không viết tay transport hay marshal, chỉ hiện thực interface ở server và gọi RPC như hàm cục bộ ở client (đo thậtCong(20,22)=42qua HTTP/2). - protobuf nhỏ và nhanh hơn JSON: đo thật cùng message, protobuf 4 byte so với JSON 15 byte (~3,75x nhỏ hơn) vì mã hóa theo số trường và bỏ tên trường; marshal 33,56 ns so với 70,89 ns (~2,1x nhanh hơn, ít cấp phát) — quan trọng ở quy mô triệu lời gọi.
- Đổi lại là tooling và khả năng debug: gRPC cần bước sinh mã và
protoc, protobuf nhị phân khó đọc bằng mắt, và cần HTTP/2 (trình duyệt phải qua grpc-web) — hợp nhất cho giao tiếp service-to-service, còn API công khai đơn giản thì REST/JSON dễ hơn.
Phần sau ta mở rộng gRPC sang kiểu gọi mạnh hơn unary: Phần sau mổ xẻ gRPC streaming — server stream, client stream, và bidirectional stream, cùng khi nào mỗi kiểu vượt trội so với unary lặp lại.