gRPC в Go
Зачем знать: gRPC — стандарт inter-service communication в современных микросервисах. Бинарный протокол поверх HTTP/2 с типизированными контрактами на Protobuf даёт меньший latency, меньший трафик и автогенерируемые клиенты. На middle-уровне ждут понимания .proto, 4 типов RPC, interceptors, error handling, TLS, и интеграции с buf/Connect.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- В Go идиоматично
- Gotchas
- Best practices
- Вопросы на собесе
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»1.1 Что такое gRPC
Заголовок раздела «1.1 Что такое gRPC»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.
1.2 Протобуф vs JSON
Заголовок раздела «1.2 Протобуф vs JSON»| Параметр | Protobuf | JSON |
|---|---|---|
| Размер | компактный (бинарный) | объёмный (текст) |
| Скорость | быстрая (генерируемый код) | медленная (рефлексия) |
| Типизация | строгая (схема) | динамическая |
| Читаемость | низкая (без tools) | высокая |
| Эволюция схемы | продумано (field numbers) | хрупко |
| Размер payload | в 2-10 раз меньше | — |
1.3 Toolchain
Заголовок раздела «1.3 Toolchain»# protoc — компилятор протобуфаbrew install protobuf
# Go-плагиныgo install google.golang.org/protobuf/cmd/protoc-gen-go@latestgo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Buf — современный замена protoc workflowbrew install bufbuild/buf/buf1.4 .proto файл
Заголовок раздела «1.4 .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;}Правила proto3
Заголовок раздела «Правила proto3»- Каждое поле имеет номер. Не меняйте номера — это ломает совместимость.
- Поля default = zero value (нет required в proto3).
repeated— список.optional(proto3.15+) — отличает «не задано» от zero value.- Имена сервисов/сообщений —
PascalCase, поля —snake_case. - Enum: первое значение =
0=XXX_UNSPECIFIED.
1.5 Генерация кода
Заголовок раздела «1.5 Генерация кода»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 (рекомендуется)
Заголовок раздела «buf (рекомендуется)»buf.yaml:
version: v2modules: - path: apilint: use: - STANDARDbreaking: use: - FILEbuf.gen.yaml:
version: v2plugins: - 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_relativebuf generate # сгенерироватьbuf lint # проверить .proto на best practicesbuf breaking --against '.git#branch=main' # проверить breaking changes1.6 4 типа RPC
Заголовок раздела «1.6 4 типа RPC»Unary (один запрос — один ответ)
Заголовок раздела «Unary (один запрос — один ответ)»rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);Самый частый случай. Аналог обычного HTTP RPC.
Server streaming (один запрос — поток ответов)
Заголовок раздела «Server streaming (один запрос — поток ответов)»rpc ListOrders(ListOrdersRequest) returns (stream Order);Use case: пагинация большого result set, real-time updates, file download.
Client streaming (поток запросов — один ответ)
Заголовок раздела «Client streaming (поток запросов — один ответ)»rpc UploadEvents(stream Event) returns (UploadEventsResponse);Use case: загрузка файла chunks, batch insert.
Bidirectional streaming
Заголовок раздела «Bidirectional streaming»rpc Chat(stream ChatMessage) returns (stream ChatMessage);Use case: chat, real-time games, telemetry. Аналог WebSocket поверх gRPC.
2. В Go идиоматично
Заголовок раздела «2. В Go идиоматично»2.1 Server implementation
Заголовок раздела «2.1 Server implementation»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, ваш код всё ещё компилируется.
2.2 Client implementation
Заголовок раздела «2.2 Client implementation»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» (пытается коннектиться сразу, что не всегда нужно).
2.3 Errors с status
Заголовок раздела «2.3 Errors с status»gRPC использует свою систему ошибок: код + сообщение + details.
import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status")
// Server: возвращаем statusreturn 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: разбираем statusresp, 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 |
| ResourceExhausted | rate limit | 429 |
| FailedPrecondition | не выполнены условия | 412 |
| Aborted | конфликт (race) | 409 |
| Unavailable | сервис недоступен | 503 |
| Unimplemented | метод не реализован | 501 |
| Internal | внутренняя ошибка | 500 |
| Unknown | неизвестная | 500 |
2.4 Context propagation
Заголовок раздела «2.4 Context propagation»Context — основа gRPC: deadline, cancel, metadata передаются автоматически.
// Client: ставим deadlinectx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()resp, err := client.GetOrder(ctx, req)// → Server получает ctx с тем же deadline
// Server: пробрасываем в downstreamfunc (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 операции прерываются2.5 Metadata (headers)
Заголовок раздела «2.5 Metadata (headers)»import "google.golang.org/grpc/metadata"
// Client: отправляем metadatamd := 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: читаем metadatamd, ok := metadata.FromIncomingContext(ctx)if !ok { return nil, status.Error(codes.InvalidArgument, "no metadata")}auth := md.Get("authorization") // []stringif len(auth) == 0 { return nil, status.Error(codes.Unauthenticated, "no token")}
// Server: возвращаем metadataheader := 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: читаем headervar hdr metadata.MDresp, err := client.GetOrder(ctx, req, grpc.Header(&hdr))2.6 Interceptors (middleware)
Заголовок раздела «2.6 Interceptors (middleware)»Interceptor — middleware для gRPC.
Unary server interceptor
Заголовок раздела «Unary server interceptor»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, ),)Stream interceptor
Заголовок раздела «Stream interceptor»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, ...),)Recovery interceptor
Заголовок раздела «Recovery interceptor»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)}Auth interceptor
Заголовок раздела «Auth interceptor»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)}Готовые middleware
Заголовок раздела «Готовые middleware»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), ),)Client interceptors
Заголовок раздела «Client interceptors»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),)2.7 TLS
Заголовок раздела «2.7 TLS»Insecure (только dev!)
Заголовок раздела «Insecure (только dev!)»grpc.WithTransportCredentials(insecure.NewCredentials())import "google.golang.org/grpc/credentials"
// Servercreds, _ := credentials.NewServerTLSFromFile("cert.pem", "key.pem")s := grpc.NewServer(grpc.Creds(creds))
// Clientcreds, _ := credentials.NewClientTLSFromFile("ca.pem", "")conn, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))mTLS (mutual TLS)
Заголовок раздела «mTLS (mutual TLS)»Когда сервер проверяет клиентский cert:
import "crypto/tls"import "crypto/x509"
// ServercaCert, _ := 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 — аналогично, добавляет свой CertificatesmTLS — стандарт для service-to-service. Идентификация через cert вместо токенов.
2.8 Health Check Protocol
Заголовок раздела «2.8 Health Check Protocol»Стандартный сервис для 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_SERVINGhs.SetServingStatus("order.v1.OrderService", grpc_health_v1.HealthCheckResponse_NOT_SERVING)Используется k8s readiness probes:
readinessProbe: grpc: port: 9090 service: order.v1.OrderService2.9 Reflection (для grpcurl)
Заголовок раздела «2.9 Reflection (для grpcurl)»import "google.golang.org/grpc/reflection"
reflection.Register(s)Позволяет grpcurl (или Postman) делать вызовы без .proto:
grpcurl -plaintext localhost:9090 listgrpcurl -plaintext localhost:9090 list order.v1.OrderServicegrpcurl -plaintext -d '{"user_id":"u1","items":[{"product_id":"p1","quantity":1}]}' \ localhost:9090 order.v1.OrderService/CreateOrder⚠️ В production reflection обычно выключают (или защищают auth) — это раскрывает API.
2.10 gRPC-Gateway (HTTP/JSON ↔ gRPC)
Заголовок раздела «2.10 gRPC-Gateway (HTTP/JSON ↔ gRPC)»Если нужно 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@latestbuf generate # генерирует HTTP gatewayGateway транслирует HTTP → gRPC. Запускается рядом с gRPC сервером:
mux := runtime.NewServeMux()orderv1.RegisterOrderServiceHandlerFromEndpoint(ctx, mux, "localhost:9090", opts)http.ListenAndServe(":8080", mux)2.11 Connect-Go
Заголовок раздела «2.11 Connect-Go»Альтернатива 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;
- Стандарт компании.
2.12 Buf: lint, breaking, generate
Заголовок раздела «2.12 Buf: lint, breaking, generate»# Lint .proto на best practicesbuf lint# Например, ловит: имена без version, отсутствие package, breaking conventions
# Detect breaking changes against main branchbuf breaking --against '.git#branch=main'# Сообщит, если: удалили поле, изменили номер, изменили тип
# Generate codebuf 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 # сгенерированный код актуален2.13 gRPC vs REST vs GraphQL
Заголовок раздела «2.13 gRPC vs REST vs GraphQL»| gRPC | REST | GraphQL | |
|---|---|---|---|
| Транспорт | HTTP/2 | HTTP/1.1+ | HTTP/1.1+ |
| Формат | Protobuf | JSON | JSON |
| Контракт | .proto (strict) | OpenAPI (часто слабый) | schema (strict) |
| Streaming | да (нативно) | SSE/WebSocket | subscriptions |
| Browser | через gateway | да | да |
| Размер | малый | большой | средний |
| Скорость | максимальная | средняя | средняя |
| Use case | service-to-service | публичные API, веб | сложные клиенты с гибкими запросами |
Правило большого пальца:
- Внутренние сервисы → gRPC.
- Публичные API → REST + OpenAPI (или Connect для гибкости).
- Сложный frontend с разными отображениями → GraphQL.
3. Gotchas
Заголовок раздела «3. Gotchas»3.1 Не зарегистрировали UnimplementedXxxServer
Заголовок раздела «3.1 Не зарегистрировали UnimplementedXxxServer»type OrderServer struct{} // ОПАСНО// orderv1.RegisterOrderServiceServer(s, &OrderServer{}) // компилится сегодня
// Завтра добавили новый RPC в .proto → ваш код перестаёт компилироватьсяРешение — embed:
type OrderServer struct { orderv1.UnimplementedOrderServiceServer}Тогда новые методы наследуют Unimplemented с codes.Unimplemented.
3.2 Поле номер изменили
Заголовок раздела «3.2 Поле номер изменили»message Order { // string id = 1; // было int64 id = 1; // стало}⚠️ Breaking change! Старые клиенты отвалятся. Правило: НЕ менять номера и типы. Только добавлять новые поля.
3.3 Удалили поле
Заголовок раздела «3.3 Удалили поле»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;}3.4 grpc.Dial vs grpc.NewClient
Заголовок раздела «3.4 grpc.Dial vs grpc.NewClient»// 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 и тестов.
3.5 Context без deadline
Заголовок раздела «3.5 Context без deadline»// ПЛОХО: клиент висит вечноctx := context.Background()resp, _ := client.GetOrder(ctx, req)
// ХОРОШО:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()Всегда ставьте deadline.
3.6 Server не уважает ctx.Done()
Заголовок раздела «3.6 Server не уважает ctx.Done()»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()}3.7 Reflection в production
Заголовок раздела «3.7 Reflection в production»reflection.Register(s) // open API discoveryРаскрывает структуру всех RPC. В production — выключайте или защищайте auth interceptor’ом.
3.8 Errors как обычные Go errors
Заголовок раздела «3.8 Errors как обычные Go errors»return nil, errors.New("not found") // ПЛОХО// клиент получит codes.UnknownВозвращайте status.Error(codes.NotFound, "..."), чтобы клиент мог разобрать код.
3.9 Message без enum_unspecified
Заголовок раздела «3.9 Message без enum_unspecified»enum Status { PENDING = 0; DONE = 1;}⚠️ Buf lint ругнётся. Правильно:
enum Status { STATUS_UNSPECIFIED = 0; STATUS_PENDING = 1; STATUS_DONE = 2;}Иначе zero value семантически неоднозначен.
3.10 Stream RPC без обработки EOF
Заголовок раздела «3.10 Stream RPC без обработки EOF»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 } // ...}3.11 Big message size
Заголовок раздела «3.11 Big message size»По умолчанию gRPC ограничивает message ~4MB. Если шлёте больше — ошибка:
grpc.NewServer( grpc.MaxRecvMsgSize(16 * 1024 * 1024), // 16MB)
grpc.NewClient(addr, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(16 * 1024 * 1024)),)Но лучше — пагинация или streaming.
3.12 Concurrent streams
Заголовок раздела «3.12 Concurrent streams»gRPC HTTP/2 multiplexing — много streams на один connection. По умолчанию max ~100. Для high-throughput подкручивайте:
grpc.MaxConcurrentStreams(1000)3.13 Keepalive
Заголовок раздела «3.13 Keepalive»Без 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, }),)3.14 Load balancing
Заголовок раздела «3.14 Load balancing»По умолчанию gRPC поддерживает round-robin на стороне клиента. Для k8s:
import _ "google.golang.org/grpc/resolver/dns"
// dns:///service.namespace.svc.cluster.local:9090conn, _ := grpc.NewClient( "dns:///service.namespace.svc.cluster.local:9090", grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),)Без этого вы попадаете в первый pod и не балансируетесь — известный gotcha k8s.
3.15 Protobuf import paths
Заголовок раздела «3.15 Protobuf import paths»import "google/protobuf/timestamp.proto";import "common/v1/error.proto";buf ищет в path-секции buf.yaml. protoc — через -I флаги. Часто проблема для новичков.
3.16 GraphQL для backend-backend
Заголовок раздела «3.16 GraphQL для backend-backend»Не путайте: GraphQL предназначен для гибких клиентских запросов. Для backend-backend он избыточен. Используйте gRPC.
4. Best practices
Заголовок раздела «4. Best practices»- Schema-first development.
.proto— source of truth. Генерируйте код, не пишите вручную. - Версионирование пакетов.
order.v1,order.v2. Никогдаorderбез версии. - Используйте buf для lint/breaking/generate. CI-проверки на breaking changes.
- Никогда не меняйте номер поля. Только добавляйте.
- Reserve удалённые поля и имена.
- Enum:
XXX_UNSPECIFIED = 0. - Embed
UnimplementedXxxServerдля forward compatibility. - TLS в production, mTLS для service-to-service.
- Interceptors для cross-cutting: logging, recovery, auth, tracing.
- status.Error с правильным кодом, не обычные
errors.New. - Health Check Protocol для k8s readiness.
- Reflection — только в dev/staging, не в production.
- Keepalive параметры для long-lived connections.
- Load balancing через DNS resolver (для k8s).
- gRPC-Gateway для web-фронтенда (или Connect-Go).
- Streaming для batch/realtime, не для CRUD.
- Context deadline везде. Клиент ставит timeout, server пробрасывает в downstream.
- Документация в .proto комментариях. Buf генерирует HTML doc.
- Метрики: gRPC code, method, duration.
grpc-ecosystem/go-grpc-middleware/metrics. - Pagination через
page_token/page_size, не offset (Google API style).
5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»-
Что такое gRPC? RPC-фреймворк от Google: HTTP/2 + Protocol Buffers + code generation. 4 типа RPC: unary, server stream, client stream, bidi.
-
Чем Protobuf отличается от JSON? Бинарный, меньше размер, быстрее парсинг, строго типизированный по схеме. Не human-readable. Хорошая поддержка эволюции схемы.
-
Какие 4 типа RPC в gRPC? Unary (1:1), Server streaming (1:N), Client streaming (N:1), Bidirectional streaming (N:M).
-
Что такое
UnimplementedXxxServer? Сгенерированный struct с default-имплементациями всех методов. Embed его в свой Server даёт forward compatibility: новые RPC в .proto не ломают компиляцию. -
Чем
grpc.Dialотличается отgrpc.NewClient? Dial — старый API, eager connect (deprecated в 1.21+). NewClient — новый, lazy, рекомендуется. -
Как gRPC обрабатывает ошибки? Через
status.Error(codes.X, "..."). Клиент получает status с кодом и сообщением, разбирает черезstatus.FromError(err). -
Какие стандартные коды gRPC? OK, Canceled, InvalidArgument, DeadlineExceeded, NotFound, AlreadyExists, PermissionDenied, Unauthenticated, ResourceExhausted, Unavailable, Internal, Unimplemented и др.
-
Что такое interceptor? gRPC middleware. Unary/Stream, Server/Client. Применяется для logging, auth, recovery, tracing.
-
Как передать данные между клиентом и сервером кроме сообщения? Через metadata: client
metadata.NewOutgoingContext, servermetadata.FromIncomingContext. -
Как сделать аутентификацию в gRPC? Через interceptor: проверяет
authorizationmetadata, кладёт user в context. Альтернативно — mTLS. -
Что такое mTLS? Mutual TLS: и сервер, и клиент проверяют сертификат друг друга. Используется для service-to-service.
-
Что такое buf? Современный toolchain для protobuf: lint, breaking change detection, code generation. Замена protoc-workflow.
-
Можно ли менять номер поля в .proto? НЕТ. Это breaking change. Только добавляйте новые поля и reserve старые номера.
-
Что такое gRPC-Gateway? Транслятор HTTP/JSON ↔ gRPC. Позволяет exposить gRPC-сервис как REST для web-клиентов.
-
Чем Connect-Go отличается от gRPC? Совместимый с gRPC, но также работает на HTTP/1.1, проще в браузере. Использует те же .proto. От Buf.
-
Что такое Health Check Protocol? Стандартный сервис
grpc.health.v1.Healthдля health probes. K8s 1.24+ поддерживаетgrpc:в probe нативно. -
Зачем Reflection? Позволяет клиентам (grpcurl, Postman) делать вызовы без .proto. Удобно в dev. В production — выключать.
-
Как балансировать gRPC-клиент в k8s?
dns:///service.namespace.svc.cluster.local+loadBalancingPolicy: round_robinв service config. Без этого — попадаешь в один pod. -
Что такое Keepalive? Параметры HTTP/2 ping, чтобы детектировать «мёртвые» connection. Без keepalive load balancer может разорвать tcp.
-
Чем gRPC быстрее REST? HTTP/2 multiplexing (много RPC на 1 connection), binary serialization (Protobuf vs JSON), header compression (HPACK), generated code (без рефлексии).
-
Когда выбрать REST вместо gRPC? Публичное API (легче для интеграции), browser-only клиенты, простой CRUD без перформанс-требований, нет инвестиций в .proto-инфраструктуру.
-
Как обновить .proto без breaking changes? Добавлять новые поля (новые номера), не менять существующие. Помечать deprecated. Reserve удалённые. buf breaking в CI.
-
Что такое context propagation в gRPC? Deadline, cancel и metadata автоматически передаются от клиента к серверу через context. Server пробрасывает в downstream-вызовы.
-
Чем server stream отличается от client stream? Server stream: client шлёт 1 request, получает N response. Client stream: client шлёт N requests, получает 1 response. Bidi — оба.
-
Где gRPC streaming применять? Batch upload (client stream), pagination/realtime updates (server stream), chat/games (bidi). Не для обычного CRUD.
-
Что такое retry в gRPC? Service config с
retryPolicy: можно автоматически повторять при codes UNAVAILABLE, DEADLINE_EXCEEDED. С exponential backoff. -
Как ограничить размер сообщения?
grpc.MaxRecvMsgSizeна сервере,grpc.MaxCallRecvMsgSizeна клиенте. По умолчанию 4MB. -
Что такое xDS / service mesh для gRPC? xDS — протокол service discovery (Envoy, Istio). gRPC поддерживает xDS native, получает endpoints из control plane.
-
Можно ли gRPC использовать в браузере? Не напрямую (браузеры не дают полный доступ к HTTP/2 frames). Через grpc-web (gateway) или Connect-Go (его умеет браузер).
-
Что такое BSR (Buf Schema Registry)? Хранилище схем .proto от Buf. Можно публиковать модули, генерировать код remote-плагинами, без локального protoc.
6. Practice
Заголовок раздела «6. Practice»-
Напишите
.protoдляOrderServiceс 4 типами RPC (unary CreateOrder, server stream ListOrders, client stream UploadEvents, bidi Chat). Сгенерируйте код через buf. -
Имплементируйте server и client для всех 4 типов. Запустите, проверьте grpcurl-ом.
-
Добавьте interceptors:
- Logging (метод, длительность, error);
- Recovery (catch panic, вернуть Internal);
- Auth (проверяет Authorization metadata, кладёт user в context).
-
Сделайте gRPC сервер с TLS. Затем — с mTLS. Проверьте, что без cert клиент не подключается.
-
Поднимите Health Check Protocol. Покажите как k8s readinessProbe c
grpc:пользуется им. -
Добавьте
grpc-gateway: тот жеOrderServiceдоступен по HTTP/JSON на/v1/orders. -
Настройте buf:
buf lintбез ошибок;buf breakingв CI. Попробуйте сломать схему (изменить номер поля), убедитесь, что CI ловит.
-
Сравните размер payload: один и тот же Order в JSON vs Protobuf. Замерьте latency для 10k RPC обоих форматов.
-
Реализуйте client с retry для codes UNAVAILABLE через service config (
grpc.WithDefaultServiceConfig). -
Опубликуйте схему в BSR (или альтернативу), сгенерируйте код через remote-плагин.
7. Источники
Заголовок раздела «7. Источники»- gRPC docs — https://grpc.io/docs/languages/go/
- Protocol Buffers — https://protobuf.dev/
- Buf — https://buf.build/docs
- google.golang.org/grpc — https://pkg.go.dev/google.golang.org/grpc
- google.golang.org/protobuf — https://pkg.go.dev/google.golang.org/protobuf
- Connect-Go — https://connectrpc.com/docs/go/
- grpc-ecosystem/go-grpc-middleware — https://github.com/grpc-ecosystem/go-grpc-middleware
- grpc-ecosystem/grpc-gateway — https://github.com/grpc-ecosystem/grpc-gateway
- Google API Design Guide — https://cloud.google.com/apis/design
- Best practices for gRPC (CNCF) — https://www.cncf.io/blog/2023/04/17/protobuf-best-practices/