Перейти к содержимому

gRPC в Go

Зачем знать: gRPC — стандарт inter-service communication в современных микросервисах. Бинарный протокол поверх HTTP/2 с типизированными контрактами на Protobuf даёт меньший latency, меньший трафик и автогенерируемые клиенты. На middle-уровне ждут понимания .proto, 4 типов RPC, interceptors, error handling, TLS, и интеграции с buf/Connect.

  1. Базовая концепция
  2. В Go идиоматично
  3. Gotchas
  4. Best practices
  5. Вопросы на собесе
  6. Practice
  7. Источники

gRPC — RPC-фреймворк от Google (2015). Стек:

  • HTTP/2 в транспорте (multiplexing, header compression, streaming);
  • Protocol Buffers для сериализации (бинарный, schema-defined);
  • Code generation клиента и сервера из .proto;
  • 4 типа RPC: unary, server stream, client stream, bidirectional stream.
ПараметрProtobufJSON
Размеркомпактный (бинарный)объёмный (текст)
Скоростьбыстрая (генерируемый код)медленная (рефлексия)
Типизациястрогая (схема)динамическая
Читаемостьнизкая (без tools)высокая
Эволюция схемыпродумано (field numbers)хрупко
Размер payloadв 2-10 раз меньше
Окно терминала
# protoc — компилятор протобуфа
brew install protobuf
# Go-плагины
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Buf — современный замена protoc workflow
brew install bufbuild/buf/buf
api/order/v1/order.proto
syntax = "proto3";
package order.v1;
option go_package = "github.com/myorg/myservice/gen/order/v1;orderv1";
import "google/protobuf/timestamp.proto";
// Сервис
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc GetOrder(GetOrderRequest) returns (Order);
rpc ListOrders(ListOrdersRequest) returns (stream Order); // server stream
rpc UploadEvents(stream Event) returns (UploadEventsResponse); // client stream
rpc Chat(stream ChatMessage) returns (stream ChatMessage); // bidi stream
}
// Сообщения
message CreateOrderRequest {
string user_id = 1;
repeated OrderItem items = 2;
}
message CreateOrderResponse {
string order_id = 1;
}
message Order {
string id = 1;
string user_id = 2;
repeated OrderItem items = 3;
OrderStatus status = 4;
Money total = 5;
google.protobuf.Timestamp created_at = 6;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
Money price = 3;
}
message Money {
int64 amount = 1;
string currency = 2;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_CANCELED = 4;
}
message GetOrderRequest {
string id = 1;
}
message ListOrdersRequest {
string user_id = 1;
int32 limit = 2;
}
message Event {
string id = 1;
string type = 2;
bytes payload = 3;
}
message UploadEventsResponse {
int32 received_count = 1;
}
message ChatMessage {
string from = 1;
string text = 2;
}
  • Каждое поле имеет номер. Не меняйте номера — это ломает совместимость.
  • Поля default = zero value (нет required в proto3).
  • repeated — список.
  • optional (proto3.15+) — отличает «не задано» от zero value.
  • Имена сервисов/сообщений — PascalCase, поля — snake_case.
  • Enum: первое значение = 0 = XXX_UNSPECIFIED.
Окно терминала
protoc \
--go_out=. \
--go-grpc_out=. \
--go_opt=paths=source_relative \
--go-grpc_opt=paths=source_relative \
api/order/v1/order.proto

Создаёт:

  • order.pb.go — типы сообщений;
  • order_grpc.pb.go — клиент и сервер stubs.

buf.yaml:

version: v2
modules:
- path: api
lint:
use:
- STANDARD
breaking:
use:
- FILE

buf.gen.yaml:

version: v2
plugins:
- remote: buf.build/protocolbuffers/go:v1.34.2
out: gen
opt: paths=source_relative
- remote: buf.build/grpc/go:v1.4.0
out: gen
opt: paths=source_relative
Окно терминала
buf generate # сгенерировать
buf lint # проверить .proto на best practices
buf breaking --against '.git#branch=main' # проверить breaking changes
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);

Самый частый случай. Аналог обычного HTTP RPC.

rpc ListOrders(ListOrdersRequest) returns (stream Order);

Use case: пагинация большого result set, real-time updates, file download.

rpc UploadEvents(stream Event) returns (UploadEventsResponse);

Use case: загрузка файла chunks, batch insert.

rpc Chat(stream ChatMessage) returns (stream ChatMessage);

Use case: chat, real-time games, telemetry. Аналог WebSocket поверх gRPC.


package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
orderv1 "github.com/myorg/myservice/gen/order/v1"
)
type OrderServer struct {
orderv1.UnimplementedOrderServiceServer // forward compatibility
}
func (s *OrderServer) CreateOrder(ctx context.Context, req *orderv1.CreateOrderRequest) (*orderv1.CreateOrderResponse, error) {
if req.UserId == "" {
return nil, status.Error(codes.InvalidArgument, "user_id is required")
}
if len(req.Items) == 0 {
return nil, status.Error(codes.InvalidArgument, "at least one item required")
}
// ... business logic
return &orderv1.CreateOrderResponse{OrderId: "order-123"}, nil
}
func (s *OrderServer) GetOrder(ctx context.Context, req *orderv1.GetOrderRequest) (*orderv1.Order, error) {
if req.Id == "" {
return nil, status.Error(codes.InvalidArgument, "id is required")
}
// ... lookup
return &orderv1.Order{Id: req.Id /* ... */}, nil
}
func (s *OrderServer) ListOrders(req *orderv1.ListOrdersRequest, stream orderv1.OrderService_ListOrdersServer) error {
for i := int32(0); i < req.Limit; i++ {
if err := stream.Send(&orderv1.Order{Id: fmt.Sprintf("order-%d", i)}); err != nil {
return err
}
}
return nil
}
func (s *OrderServer) UploadEvents(stream orderv1.OrderService_UploadEventsServer) error {
var count int32
for {
evt, err := stream.Recv()
if err == io.EOF {
return stream.SendAndClose(&orderv1.UploadEventsResponse{ReceivedCount: count})
}
if err != nil {
return err
}
_ = evt
count++
}
}
func (s *OrderServer) Chat(stream orderv1.OrderService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// echo обратно
if err := stream.Send(&orderv1.ChatMessage{From: "server", Text: msg.Text}); err != nil {
return err
}
}
}
func main() {
lis, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
orderv1.RegisterOrderServiceServer(s, &OrderServer{})
log.Println("gRPC server listening on :9090")
if err := s.Serve(lis); err != nil {
log.Fatal(err)
}
}

⚠️ Всегда встраивайте UnimplementedXxxServer — это даёт forward compatibility: если в .proto добавили новый RPC, ваш код всё ещё компилируется.

package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
orderv1 "github.com/myorg/myservice/gen/order/v1"
)
func main() {
// Go 1.21+: grpc.NewClient (раньше grpc.Dial)
conn, err := grpc.NewClient("localhost:9090",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := orderv1.NewOrderServiceClient(conn)
// Unary
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.CreateOrder(ctx, &orderv1.CreateOrderRequest{
UserId: "user-1",
Items: []*orderv1.OrderItem{{ProductId: "p1", Quantity: 2}},
})
if err != nil {
log.Fatal(err)
}
log.Println("created:", resp.OrderId)
// Server stream
stream, err := client.ListOrders(ctx, &orderv1.ListOrdersRequest{UserId: "user-1", Limit: 10})
if err != nil {
log.Fatal(err)
}
for {
order, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
log.Println("order:", order.Id)
}
}

⚠️ В Go 1.21+ предпочтительно grpc.NewClient(target, opts...) (lazy connection). Старый grpc.Dial тоже работает, но «eager» (пытается коннектиться сразу, что не всегда нужно).

gRPC использует свою систему ошибок: код + сообщение + details.

import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Server: возвращаем status
return nil, status.Error(codes.NotFound, "order not found")
// С details (например, errdetails.BadRequest)
import "google.golang.org/genproto/googleapis/rpc/errdetails"
st := status.New(codes.InvalidArgument, "validation failed")
br := &errdetails.BadRequest{
FieldViolations: []*errdetails.BadRequest_FieldViolation{
{Field: "email", Description: "invalid format"},
},
}
st, _ = st.WithDetails(br)
return nil, st.Err()
// Client: разбираем status
resp, err := client.GetOrder(ctx, req)
if err != nil {
st, ok := status.FromError(err)
if !ok {
log.Println("non-grpc error:", err)
}
switch st.Code() {
case codes.NotFound:
log.Println("not found")
case codes.InvalidArgument:
log.Println("validation error")
case codes.DeadlineExceeded:
log.Println("timeout")
}
}

Стандартные коды (RFC):

CodeСемантикаHTTP-аналог
OKуспех200
Canceledклиент отменил499
InvalidArgumentплохой ввод400
DeadlineExceededтаймаут504
NotFoundне найден404
AlreadyExistsконфликт409
PermissionDeniedнет прав403
Unauthenticatedнет аутентификации401
ResourceExhaustedrate limit429
FailedPreconditionне выполнены условия412
Abortedконфликт (race)409
Unavailableсервис недоступен503
Unimplementedметод не реализован501
Internalвнутренняя ошибка500
Unknownнеизвестная500

Context — основа gRPC: deadline, cancel, metadata передаются автоматически.

// Client: ставим deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetOrder(ctx, req)
// → Server получает ctx с тем же deadline
// Server: пробрасываем в downstream
func (s *OrderServer) GetOrder(ctx context.Context, req *orderv1.GetOrderRequest) (*orderv1.Order, error) {
return s.repo.GetByID(ctx, req.Id) // ctx с deadline пробросился
}
// Cancel: клиент отменил → ctx.Done() closed → server-side операции прерываются
import "google.golang.org/grpc/metadata"
// Client: отправляем metadata
md := metadata.New(map[string]string{
"authorization": "Bearer " + token,
"x-request-id": uuid.NewString(),
})
ctx = metadata.NewOutgoingContext(ctx, md)
resp, err := client.GetOrder(ctx, req)
// Server: читаем metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.InvalidArgument, "no metadata")
}
auth := md.Get("authorization") // []string
if len(auth) == 0 {
return nil, status.Error(codes.Unauthenticated, "no token")
}
// Server: возвращаем metadata
header := metadata.Pairs("x-server-version", "1.2.3")
grpc.SetHeader(ctx, header)
trailer := metadata.Pairs("x-processing-time-ms", "42")
grpc.SetTrailer(ctx, trailer)
// Client: читаем header
var hdr metadata.MD
resp, err := client.GetOrder(ctx, req, grpc.Header(&hdr))

Interceptor — middleware для gRPC.

type UnaryServerInterceptor func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error)
func LoggingInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("%s took %v err=%v", info.FullMethod, time.Since(start), err)
return resp, err
}
s := grpc.NewServer(
grpc.ChainUnaryInterceptor(
LoggingInterceptor,
RecoveryInterceptor,
AuthInterceptor,
),
)
func LoggingStreamInterceptor(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
start := time.Now()
err := handler(srv, ss)
log.Printf("%s stream took %v", info.FullMethod, time.Since(start))
return err
}
s := grpc.NewServer(
grpc.ChainStreamInterceptor(LoggingStreamInterceptor, ...),
)
func RecoveryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in %s: %v\n%s", info.FullMethod, r, debug.Stack())
err = status.Error(codes.Internal, "internal error")
}
}()
return handler(ctx, req)
}
func AuthInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
if info.FullMethod == "/order.v1.OrderService/Healthcheck" {
return handler(ctx, req)
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "no token")
}
userID, err := verifyToken(tokens[0])
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
ctx = context.WithValue(ctx, userIDKey, userID)
return handler(ctx, req)
}

go-grpc-middleware (grpc-ecosystem) — набор готовых:

import (
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
)
s := grpc.NewServer(
grpc.ChainUnaryInterceptor(
logging.UnaryServerInterceptor(InterceptorLogger(logger)),
recovery.UnaryServerInterceptor(),
auth.UnaryServerInterceptor(myAuth),
),
)
func LoggingClientInterceptor(
ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
log.Printf("client %s took %v", method, time.Since(start))
return err
}
conn, _ := grpc.NewClient(addr,
grpc.WithChainUnaryInterceptor(LoggingClientInterceptor),
)
grpc.WithTransportCredentials(insecure.NewCredentials())
import "google.golang.org/grpc/credentials"
// Server
creds, _ := credentials.NewServerTLSFromFile("cert.pem", "key.pem")
s := grpc.NewServer(grpc.Creds(creds))
// Client
creds, _ := credentials.NewClientTLSFromFile("ca.pem", "")
conn, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))

Когда сервер проверяет клиентский cert:

import "crypto/tls"
import "crypto/x509"
// Server
caCert, _ := os.ReadFile("ca.pem")
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCert)
serverCert, _ := tls.LoadX509KeyPair("server-cert.pem", "server-key.pem")
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caPool,
}
creds := credentials.NewTLS(tlsCfg)
s := grpc.NewServer(grpc.Creds(creds))
// Client — аналогично, добавляет свой Certificates

mTLS — стандарт для service-to-service. Идентификация через cert вместо токенов.

Стандартный сервис для health probes (grpc.health.v1.Health):

import (
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
)
hs := health.NewServer()
hs.SetServingStatus("order.v1.OrderService", grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(s, hs)
// При shutdown: ставим NOT_SERVING
hs.SetServingStatus("order.v1.OrderService", grpc_health_v1.HealthCheckResponse_NOT_SERVING)

Используется k8s readiness probes:

readinessProbe:
grpc:
port: 9090
service: order.v1.OrderService
import "google.golang.org/grpc/reflection"
reflection.Register(s)

Позволяет grpcurl (или Postman) делать вызовы без .proto:

Окно терминала
grpcurl -plaintext localhost:9090 list
grpcurl -plaintext localhost:9090 list order.v1.OrderService
grpcurl -plaintext -d '{"user_id":"u1","items":[{"product_id":"p1","quantity":1}]}' \
localhost:9090 order.v1.OrderService/CreateOrder

⚠️ В production reflection обычно выключают (или защищают auth) — это раскрывает API.

Если нужно exposить gRPC-сервис как REST/JSON (для web-фронтенда):

import "google/api/annotations.proto";
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse) {
option (google.api.http) = {
post: "/v1/orders"
body: "*"
};
}
}
Окно терминала
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
buf generate # генерирует HTTP gateway

Gateway транслирует HTTP → gRPC. Запускается рядом с gRPC сервером:

mux := runtime.NewServeMux()
orderv1.RegisterOrderServiceHandlerFromEndpoint(ctx, mux, "localhost:9090", opts)
http.ListenAndServe(":8080", mux)

Альтернатива gRPC от Buf: connectrpc.com/connect. Совместим с gRPC, но также работает на HTTP/1.1, гораздо проще в браузере. Использует тот же proto, генерируется protoc-gen-connect-go.

import "connectrpc.com/connect"
server := orderv1connect.NewOrderServiceHandler(impl)
mux := http.NewServeMux()
mux.Handle(orderv1connect.OrderServiceHandlerPattern, server)
http.ListenAndServe(":8080", h2c.NewHandler(mux, &http2.Server{}))

Когда выбрать Connect:

  • Нужна совместимость с web-браузером (без отдельного gateway);
  • Хочется простой HTTP/JSON-API + бинарный gRPC;
  • Buf экосистема.

Когда gRPC «обычный»:

  • Чисто backend-backend;
  • Нужна максимальная performance;
  • Стандарт компании.
Окно терминала
# Lint .proto на best practices
buf lint
# Например, ловит: имена без version, отсутствие package, breaking conventions
# Detect breaking changes against main branch
buf breaking --against '.git#branch=main'
# Сообщит, если: удалили поле, изменили номер, изменили тип
# Generate code
buf generate
# Push schema to BSR (Buf Schema Registry)
buf push

В CI:

- run: buf lint
- run: buf breaking --against 'https://github.com/myorg/repo.git#branch=main'
- run: buf generate
- run: git diff --exit-code # сгенерированный код актуален
gRPCRESTGraphQL
ТранспортHTTP/2HTTP/1.1+HTTP/1.1+
ФорматProtobufJSONJSON
Контракт.proto (strict)OpenAPI (часто слабый)schema (strict)
Streamingда (нативно)SSE/WebSocketsubscriptions
Browserчерез gatewayдада
Размермалыйбольшойсредний
Скоростьмаксимальнаясредняясредняя
Use caseservice-to-serviceпубличные API, вебсложные клиенты с гибкими запросами

Правило большого пальца:

  • Внутренние сервисы → gRPC.
  • Публичные API → REST + OpenAPI (или Connect для гибкости).
  • Сложный frontend с разными отображениями → GraphQL.

type OrderServer struct{} // ОПАСНО
// orderv1.RegisterOrderServiceServer(s, &OrderServer{}) // компилится сегодня
// Завтра добавили новый RPC в .proto → ваш код перестаёт компилироваться

Решение — embed:

type OrderServer struct {
orderv1.UnimplementedOrderServiceServer
}

Тогда новые методы наследуют Unimplemented с codes.Unimplemented.

message Order {
// string id = 1; // было
int64 id = 1; // стало
}

⚠️ Breaking change! Старые клиенты отвалятся. Правило: НЕ менять номера и типы. Только добавлять новые поля.

message Order {
// string deprecated_field = 5; // удалили
string new_field = 6;
}

Если поле кто-то использует — данные потеряются. Правильный подход — deprecate:

message Order {
string deprecated_field = 5 [deprecated = true];
string new_field = 6;
}

И reserve номер, чтобы случайно не использовать:

message Order {
reserved 5;
reserved "deprecated_field";
string new_field = 6;
}
// Go < 1.21: grpc.Dial (deprecated)
conn, _ := grpc.Dial(addr, opts...)
// Go 1.21+: grpc.NewClient (рекомендуется)
conn, _ := grpc.NewClient(addr, opts...)

Отличие: Dial делает «eager» connect; NewClient — lazy. NewClient проще для DI и тестов.

// ПЛОХО: клиент висит вечно
ctx := context.Background()
resp, _ := client.GetOrder(ctx, req)
// ХОРОШО:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

Всегда ставьте deadline.

func (s *Server) Slow(ctx context.Context, req *X) (*Y, error) {
time.Sleep(1 * time.Minute) // НЕ уважает ctx
return &Y{}, nil
}

Если клиент отменил, sleep всё равно завершится. В реальной работе всегда:

select {
case <-time.After(1 * time.Minute):
case <-ctx.Done():
return nil, ctx.Err()
}
reflection.Register(s) // open API discovery

Раскрывает структуру всех RPC. В production — выключайте или защищайте auth interceptor’ом.

return nil, errors.New("not found") // ПЛОХО
// клиент получит codes.Unknown

Возвращайте status.Error(codes.NotFound, "..."), чтобы клиент мог разобрать код.

enum Status {
PENDING = 0;
DONE = 1;
}

⚠️ Buf lint ругнётся. Правильно:

enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_PENDING = 1;
STATUS_DONE = 2;
}

Иначе zero value семантически неоднозначен.

for {
msg, err := stream.Recv()
if err != nil {
log.Fatal(err) // ПЛОХО: EOF — нормальное завершение
}
}
for {
msg, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
// ...
}

По умолчанию gRPC ограничивает message ~4MB. Если шлёте больше — ошибка:

grpc.NewServer(
grpc.MaxRecvMsgSize(16 * 1024 * 1024), // 16MB
)
grpc.NewClient(addr,
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(16 * 1024 * 1024)),
)

Но лучше — пагинация или streaming.

gRPC HTTP/2 multiplexing — много streams на один connection. По умолчанию max ~100. Для high-throughput подкручивайте:

grpc.MaxConcurrentStreams(1000)

Без keepalive connection может «зависнуть» (load balancer таймаутит):

import "google.golang.org/grpc/keepalive"
grpc.NewServer(grpc.KeepaliveParams(keepalive.ServerParameters{
Time: 10 * time.Second,
Timeout: 3 * time.Second,
}))
grpc.NewClient(addr,
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 10 * time.Second,
Timeout: 3 * time.Second,
PermitWithoutStream: true,
}),
)

По умолчанию gRPC поддерживает round-robin на стороне клиента. Для k8s:

import _ "google.golang.org/grpc/resolver/dns"
// dns:///service.namespace.svc.cluster.local:9090
conn, _ := grpc.NewClient(
"dns:///service.namespace.svc.cluster.local:9090",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
)

Без этого вы попадаете в первый pod и не балансируетесь — известный gotcha k8s.

import "google/protobuf/timestamp.proto";
import "common/v1/error.proto";

buf ищет в path-секции buf.yaml. protoc — через -I флаги. Часто проблема для новичков.

Не путайте: GraphQL предназначен для гибких клиентских запросов. Для backend-backend он избыточен. Используйте gRPC.


  1. Schema-first development. .proto — source of truth. Генерируйте код, не пишите вручную.
  2. Версионирование пакетов. order.v1, order.v2. Никогда order без версии.
  3. Используйте buf для lint/breaking/generate. CI-проверки на breaking changes.
  4. Никогда не меняйте номер поля. Только добавляйте.
  5. Reserve удалённые поля и имена.
  6. Enum: XXX_UNSPECIFIED = 0.
  7. Embed UnimplementedXxxServer для forward compatibility.
  8. TLS в production, mTLS для service-to-service.
  9. Interceptors для cross-cutting: logging, recovery, auth, tracing.
  10. status.Error с правильным кодом, не обычные errors.New.
  11. Health Check Protocol для k8s readiness.
  12. Reflection — только в dev/staging, не в production.
  13. Keepalive параметры для long-lived connections.
  14. Load balancing через DNS resolver (для k8s).
  15. gRPC-Gateway для web-фронтенда (или Connect-Go).
  16. Streaming для batch/realtime, не для CRUD.
  17. Context deadline везде. Клиент ставит timeout, server пробрасывает в downstream.
  18. Документация в .proto комментариях. Buf генерирует HTML doc.
  19. Метрики: gRPC code, method, duration. grpc-ecosystem/go-grpc-middleware/metrics.
  20. Pagination через page_token/page_size, не offset (Google API style).

  1. Что такое gRPC? RPC-фреймворк от Google: HTTP/2 + Protocol Buffers + code generation. 4 типа RPC: unary, server stream, client stream, bidi.

  2. Чем Protobuf отличается от JSON? Бинарный, меньше размер, быстрее парсинг, строго типизированный по схеме. Не human-readable. Хорошая поддержка эволюции схемы.

  3. Какие 4 типа RPC в gRPC? Unary (1:1), Server streaming (1:N), Client streaming (N:1), Bidirectional streaming (N:M).

  4. Что такое UnimplementedXxxServer? Сгенерированный struct с default-имплементациями всех методов. Embed его в свой Server даёт forward compatibility: новые RPC в .proto не ломают компиляцию.

  5. Чем grpc.Dial отличается от grpc.NewClient? Dial — старый API, eager connect (deprecated в 1.21+). NewClient — новый, lazy, рекомендуется.

  6. Как gRPC обрабатывает ошибки? Через status.Error(codes.X, "..."). Клиент получает status с кодом и сообщением, разбирает через status.FromError(err).

  7. Какие стандартные коды gRPC? OK, Canceled, InvalidArgument, DeadlineExceeded, NotFound, AlreadyExists, PermissionDenied, Unauthenticated, ResourceExhausted, Unavailable, Internal, Unimplemented и др.

  8. Что такое interceptor? gRPC middleware. Unary/Stream, Server/Client. Применяется для logging, auth, recovery, tracing.

  9. Как передать данные между клиентом и сервером кроме сообщения? Через metadata: client metadata.NewOutgoingContext, server metadata.FromIncomingContext.

  10. Как сделать аутентификацию в gRPC? Через interceptor: проверяет authorization metadata, кладёт user в context. Альтернативно — mTLS.

  11. Что такое mTLS? Mutual TLS: и сервер, и клиент проверяют сертификат друг друга. Используется для service-to-service.

  12. Что такое buf? Современный toolchain для protobuf: lint, breaking change detection, code generation. Замена protoc-workflow.

  13. Можно ли менять номер поля в .proto? НЕТ. Это breaking change. Только добавляйте новые поля и reserve старые номера.

  14. Что такое gRPC-Gateway? Транслятор HTTP/JSON ↔ gRPC. Позволяет exposить gRPC-сервис как REST для web-клиентов.

  15. Чем Connect-Go отличается от gRPC? Совместимый с gRPC, но также работает на HTTP/1.1, проще в браузере. Использует те же .proto. От Buf.

  16. Что такое Health Check Protocol? Стандартный сервис grpc.health.v1.Health для health probes. K8s 1.24+ поддерживает grpc: в probe нативно.

  17. Зачем Reflection? Позволяет клиентам (grpcurl, Postman) делать вызовы без .proto. Удобно в dev. В production — выключать.

  18. Как балансировать gRPC-клиент в k8s? dns:///service.namespace.svc.cluster.local + loadBalancingPolicy: round_robin в service config. Без этого — попадаешь в один pod.

  19. Что такое Keepalive? Параметры HTTP/2 ping, чтобы детектировать «мёртвые» connection. Без keepalive load balancer может разорвать tcp.

  20. Чем gRPC быстрее REST? HTTP/2 multiplexing (много RPC на 1 connection), binary serialization (Protobuf vs JSON), header compression (HPACK), generated code (без рефлексии).

  21. Когда выбрать REST вместо gRPC? Публичное API (легче для интеграции), browser-only клиенты, простой CRUD без перформанс-требований, нет инвестиций в .proto-инфраструктуру.

  22. Как обновить .proto без breaking changes? Добавлять новые поля (новые номера), не менять существующие. Помечать deprecated. Reserve удалённые. buf breaking в CI.

  23. Что такое context propagation в gRPC? Deadline, cancel и metadata автоматически передаются от клиента к серверу через context. Server пробрасывает в downstream-вызовы.

  24. Чем server stream отличается от client stream? Server stream: client шлёт 1 request, получает N response. Client stream: client шлёт N requests, получает 1 response. Bidi — оба.

  25. Где gRPC streaming применять? Batch upload (client stream), pagination/realtime updates (server stream), chat/games (bidi). Не для обычного CRUD.

  26. Что такое retry в gRPC? Service config с retryPolicy: можно автоматически повторять при codes UNAVAILABLE, DEADLINE_EXCEEDED. С exponential backoff.

  27. Как ограничить размер сообщения? grpc.MaxRecvMsgSize на сервере, grpc.MaxCallRecvMsgSize на клиенте. По умолчанию 4MB.

  28. Что такое xDS / service mesh для gRPC? xDS — протокол service discovery (Envoy, Istio). gRPC поддерживает xDS native, получает endpoints из control plane.

  29. Можно ли gRPC использовать в браузере? Не напрямую (браузеры не дают полный доступ к HTTP/2 frames). Через grpc-web (gateway) или Connect-Go (его умеет браузер).

  30. Что такое BSR (Buf Schema Registry)? Хранилище схем .proto от Buf. Можно публиковать модули, генерировать код remote-плагинами, без локального protoc.


  1. Напишите .proto для OrderService с 4 типами RPC (unary CreateOrder, server stream ListOrders, client stream UploadEvents, bidi Chat). Сгенерируйте код через buf.

  2. Имплементируйте server и client для всех 4 типов. Запустите, проверьте grpcurl-ом.

  3. Добавьте interceptors:

    • Logging (метод, длительность, error);
    • Recovery (catch panic, вернуть Internal);
    • Auth (проверяет Authorization metadata, кладёт user в context).
  4. Сделайте gRPC сервер с TLS. Затем — с mTLS. Проверьте, что без cert клиент не подключается.

  5. Поднимите Health Check Protocol. Покажите как k8s readinessProbe c grpc: пользуется им.

  6. Добавьте grpc-gateway: тот же OrderService доступен по HTTP/JSON на /v1/orders.

  7. Настройте buf:

    • buf lint без ошибок;
    • buf breaking в CI. Попробуйте сломать схему (изменить номер поля), убедитесь, что CI ловит.
  8. Сравните размер payload: один и тот же Order в JSON vs Protobuf. Замерьте latency для 10k RPC обоих форматов.

  9. Реализуйте client с retry для codes UNAVAILABLE через service config (grpc.WithDefaultServiceConfig).

  10. Опубликуйте схему в BSR (или альтернативу), сгенерируйте код через remote-плагин.