Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Backend · Уровень: Middle+/Senior ## TL;DR - **gRPC** — RPC-фреймворк поверх **HTTP/2**, использующий **Protocol Buffers** как IDL и формат сериализации (бинарный, schema-first). Один TCP-коннект мультиплексирует множество параллельных стримов (нет head-of-line blocking на уровне приложения). - **4 типа RPC**: unary (1→1), server streaming (1→N), client streaming (N→1), bidirectional streaming (N→M, полнодуплексный). - **Интерсепторы** = middleware. Отдельные типы для unary/stream и для client/server. Цепочки строятся через `grpc.ChainUnaryInterceptor` / `ChainStreamInterceptor`. Типичные: logging, auth, recovery (panic→`codes.Internal`), metrics, tracing. - **Контекст и дедлайны**: `context.WithTimeout` на клиенте → дедлайн сериализуется в заголовок `grpc-timeout` → сервер получает его в `ctx` → **дедлайн распространяется по цепочке** при дальнейших вызовах. Отмена клиента доходит до сервера (`ctx.Done()`). - **Метаданные** (`metadata.MD`) — пары ключ/значение (≈HTTP-заголовки). `FromIncomingContext` (чтение на сервере), `NewOutgoingContext` (отправка с клиента). Различают **headers** (до тела) и **trailers** (после тела, для стримов критично). - **Error model**: ошибки = `status.Status` с `codes.Code` + message + опциональные `details` (`google.rpc.*`, напр. `ErrorInfo`, `BadRequest`). `status.Errorf(codes.NotFound, ...)`. Есть стандартный маппинг codes ↔ HTTP. - **gRPC vs REST**: gRPC — для внутренних сервис-сервис вызовов (производительность, строгий контракт, streaming); REST/JSON — для публичных/браузерных API. Браузер не умеет gRPC напрямую → gRPC-Web / gRPC-Gateway. ## Теория ### 1. Что такое gRPC gRPC — это контрактно-ориентированный (schema-first) RPC-фреймворк. Ключевые составляющие: - **HTTP/2 как транспорт**: бинарный фреймовый протокол с мультиплексированием стримов поверх одного TCP-соединения, сжатием заголовков (HPACK), flow control на уровне стрима и коннекта, server push (gRPC его не использует). Мультиплексирование убирает application-level head-of-line blocking (HTTP/1.1 требовал по соединению на запрос или pipelining). Но: TCP-level HOL blocking остаётся (это решает уже HTTP/3 / QUIC, который gRPC поддерживает экспериментально). - **Protocol Buffers** как IDL и wire-формат: компактный бинарный формат с tag-number полями, обратно/прямо совместимый при правильной эволюции схемы. Кодогенерация (`protoc` + `protoc-gen-go` + `protoc-gen-go-grpc`) даёт типобезопасные клиент и сервер. - **Один RPC = один HTTP/2-стрим**. Метод кодируется как путь `:path = /package.Service/Method`. Тело — `length-prefixed message`: 1 байт compressed-flag + 4 байта big-endian длины + payload. ```proto syntax = "proto3"; package user.v1; option go_package = "example.com/gen/user/v1;userv1"; message GetUserRequest { string id = 1; } message User { string id = 1; string name = 2; int32 age = 3; } service UserService { rpc GetUser(GetUserRequest) returns (User); } ``` ```bash protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ user/v1/user.proto ``` ### 2. Четыре типа RPC | Тип | Запрос | Ответ | Сигнатура (упрощённо) | Применение | |-----|--------|-------|------------------------|------------| | Unary | 1 | 1 | `Get(ctx, req) (resp, err)` | обычный запрос-ответ | | Server streaming | 1 | N | `List(ctx, req) (stream, err)` | выдача списка/фида, прогресс | | Client streaming | N | 1 | `Upload(ctx) (stream, err)` | загрузка чанков, batch upsert | | Bidi streaming | N | M | `Chat(ctx) (stream, err)` | чат, real-time, двусторонний обмен | ```proto service Demo { rpc Unary(Req) returns (Resp); rpc ServerStream(Req) returns (stream Resp); rpc ClientStream(stream Req) returns (Resp); rpc BidiStream(stream Req) returns (stream Resp); } ``` **Unary (сервер):** ```go func (s *server) Unary(ctx context.Context, req *pb.Req) (*pb.Resp, error) { return &pb.Resp{Value: req.GetValue()}, nil } ``` **Server streaming (сервер):** возвращаем ошибку из метода, чтобы завершить стрим; `Send` можно вызывать много раз. ```go func (s *server) ServerStream(req *pb.Req, stream pb.Demo_ServerStreamServer) error { for i := 0; i < 10; i++ { if err := stream.Context().Err(); err != nil { // клиент отменил/дедлайн return err } if err := stream.Send(&pb.Resp{Value: int32(i)}); err != nil { return err // транспорт сломался } } return nil // завершение стрима = io.EOF на клиенте } ``` **Client streaming (сервер):** читаем до `io.EOF`, затем отправляем один ответ через `SendAndClose`. ```go func (s *server) ClientStream(stream pb.Demo_ClientStreamServer) error { var sum int32 for { req, err := stream.Recv() if errors.Is(err, io.EOF) { return stream.SendAndClose(&pb.Resp{Value: sum}) } if err != nil { return err } sum += req.GetValue() } } ``` **Bidirectional streaming (сервер):** Recv и Send независимы (полный дуплекс). Часто Send делают из отдельной горутины. ```go func (s *server) BidiStream(stream pb.Demo_BidiStreamServer) error { for { req, err := stream.Recv() if errors.Is(err, io.EOF) { return nil // клиент закрыл свою сторону } if err != nil { return err } if err := stream.Send(&pb.Resp{Value: req.GetValue() * 2}); err != nil { return err } } } ``` **Клиент (server streaming):** ```go stream, err := client.ServerStream(ctx, &pb.Req{}) if err != nil { /* ошибка установки стрима */ } for { resp, err := stream.Recv() if errors.Is(err, io.EOF) { break } // нормальное завершение if err != nil { st, _ := status.FromError(err) // здесь приходит статус-ошибка сервера log.Printf("stream err: %v %v", st.Code(), st.Message()) break } use(resp) } ``` Важно: ошибка статуса в стримах **приходит из `Recv()`**, а не из вызова метода. Сам вызов `client.ServerStream(...)` обычно лишь открывает стрим. ### 3. Интерсепторы (middleware) Четыре базовых типа: server-unary, server-stream, client-unary, client-stream. ```go // Server unary type UnaryServerInterceptor func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) // Server stream type StreamServerInterceptor func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error ``` **Цепочки** (порядок применения = порядок аргументов; первый — самый внешний): ```go srv := grpc.NewServer( grpc.ChainUnaryInterceptor(recoveryUnary, loggingUnary, authUnary), grpc.ChainStreamInterceptor(recoveryStream, loggingStream, authStream), ) ``` **Logging + metrics:** ```go func loggingUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { start := time.Now() resp, err := handler(ctx, req) code := status.Code(err) log.Printf("method=%s code=%s dur=%s", info.FullMethod, code, time.Since(start)) metrics.ObserveRPC(info.FullMethod, code.String(), time.Since(start)) return resp, err } ``` **Auth** (читаем токен из метаданных, прокидываем claims в context): ```go func authUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, status.Error(codes.Unauthenticated, "no metadata") } vals := md.Get("authorization") if len(vals) == 0 { return nil, status.Error(codes.Unauthenticated, "missing token") } claims, err := verify(vals[0]) if err != nil { return nil, status.Error(codes.Unauthenticated, "invalid token") } ctx = context.WithValue(ctx, claimsKey{}, claims) return handler(ctx, req) } ``` **Recovery** (panic → `codes.Internal`, чтобы один паникнувший RPC не уронил процесс): ```go func recoveryUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { defer func() { if r := recover(); r != nil { log.Printf("panic in %s: %v\n%s", info.FullMethod, r, debug.Stack()) err = status.Errorf(codes.Internal, "internal error") } }() return handler(ctx, req) } ``` **Stream-интерсептор с подменой context.** В стрим-интерсепторе нет прямого доступа к `ctx` хендлера — `ServerStream` имеет метод `Context()`. Чтобы прокинуть значения вниз, оборачивают `ServerStream`: ```go type wrappedStream struct { grpc.ServerStream ctx context.Context } func (w *wrappedStream) Context() context.Context { return w.ctx } func authStream(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { ctx, err := authenticate(ss.Context()) if err != nil { return err } return handler(srv, &wrappedStream{ServerStream: ss, ctx: ctx}) } ``` **Client-side** интерсепторы (для исходящих вызовов: retry, добавление токена, трейсинг): ```go func authClientUnary(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { ctx = metadata.AppendToOutgoingContext(ctx, "authorization", token()) return invoker(ctx, method, req, reply, cc, opts...) } conn, _ := grpc.NewClient(addr, grpc.WithChainUnaryInterceptor(authClientUnary)) ``` На практике почти всегда берут `github.com/grpc-ecosystem/go-grpc-middleware` (v2): готовые recovery/logging/auth/retry/ratelimit/validator. ### 4. Контекст и дедлайны Дедлайн задаётся на клиенте и **передаётся серверу через wire** (заголовок HTTP/2 `grpc-timeout`, напр. `100m`). Сервер видит его как deadline в `ctx`. ```go ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "42"}) if status.Code(err) == codes.DeadlineExceeded { // дедлайн истёк } ``` **Propagation (распространение дедлайна по цепочке).** Если сервис A с дедлайном 200мс вызывает сервис B, передавая полученный `ctx`, то оставшийся бюджет времени (минус прошедшее) уходит в B. Так дедлайн каскадируется по всей цепочке вызовов, и B не будет работать дольше, чем у A осталось времени. ```go func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) { // ctx уже содержит дедлайн клиента; пробрасываем его дальше profile, err := s.profileClient.GetProfile(ctx, &pb.GetProfileRequest{Id: req.Id}) ... } ``` **Отмена.** Если клиент отменяет вызов (cancel) или рвётся соединение, сервер получает `ctx.Err()` (`context.Canceled`) — нужно периодически проверять `ctx.Err()` / `ctx.Done()` в долгих операциях и в стримах (`stream.Context()`), чтобы не делать лишнюю работу. Анти-паттерн: вызывать downstream-сервис с `context.Background()` вместо входящего `ctx` — теряется дедлайн и отмена; downstream продолжит работать, когда клиент уже ушёл. ### 5. Метаданные `metadata.MD` — это `map[string][]string`. Ключи приводятся к нижнему регистру. Бинарные значения — суффикс `-bin` (значение base64-кодируется на wire). Зарезервированы ключи `grpc-*`. ```go // КЛИЕНТ: отправка исходящих метаданных md := metadata.Pairs("authorization", "Bearer xxx", "x-request-id", reqID) ctx = metadata.NewOutgoingContext(ctx, md) // или инкрементально: ctx = metadata.AppendToOutgoingContext(ctx, "x-tenant", "acme") // СЕРВЕР: чтение входящих метаданных md, ok := metadata.FromIncomingContext(ctx) reqID := first(md.Get("x-request-id")) ``` **Headers vs Trailers.** | | Headers | Trailers | |---|---------|----------| | Когда | до тела ответа | после тела ответа | | Когда отправить | `SetHeader`/`SendHeader` до первого `Send` | `SetTrailer`, отправляются автоматически при завершении | | Зачем в стримах | мета до начала потока | мета по итогам (метрики, финальный статус) | | Сам gRPC | — | статус-код/сообщение передаются именно в trailers | ```go // Сервер grpc.SetHeader(ctx, metadata.Pairs("x-served-by", host)) // header grpc.SetTrailer(ctx, metadata.Pairs("x-rows-scanned", "100")) // trailer // Клиент: получить header/trailer от unary-вызова var hdr, trl metadata.MD resp, err := client.GetUser(ctx, req, grpc.Header(&hdr), grpc.Trailer(&trl)) ``` Важная деталь: т.к. статус gRPC передаётся в trailers, при HTTP/1.1 он бы не работал — поэтому gRPC требует именно HTTP/2 (trailers). Для server streaming статус-код приходит после всех сообщений — отсюда модель «`Recv()` до `io.EOF`, ошибка в последнем `Recv()`». ### 6. Error model Ошибка gRPC = `*status.Status`: код (`codes.Code`), сообщение, и опциональные `details` (произвольные protobuf-сообщения, обычно из `google.rpc`). ```go // Создание return nil, status.Errorf(codes.NotFound, "user %s not found", id) return nil, status.Error(codes.InvalidArgument, "id is required") // Разбор на клиенте st, ok := status.FromError(err) switch st.Code() { case codes.NotFound: ... case codes.DeadlineExceeded: ... case codes.Unavailable: // транзиентно, можно ретраить } ``` **Основные коды** (`google.golang.org/grpc/codes`): | Code | Семантика | Типичная причина | HTTP-маппинг | |------|-----------|------------------|--------------| | `OK` (0) | успех | — | 200 | | `Canceled` (1) | отмена клиентом | client cancel | 499 | | `Unknown` (2) | неизвестная | panic, не-status error | 500 | | `InvalidArgument` (3) | плохой запрос | валидация | 400 | | `DeadlineExceeded` (4) | дедлайн истёк | таймаут | 504 | | `NotFound` (5) | нет ресурса | — | 404 | | `AlreadyExists` (6) | конфликт создания | дубликат | 409 | | `PermissionDenied` (7) | нет прав | RBAC | 403 | | `ResourceExhausted` (8) | лимит/квота | rate limit | 429 | | `FailedPrecondition` (9) | состояние не позволяет | — | 400 | | `Aborted` (10) | конфликт конкурентности | optimistic lock | 409 | | `OutOfRange` (11) | выход за границы | пагинация | 400 | | `Unimplemented` (12) | метод не реализован | старый сервер | 501 | | `Internal` (13) | внутренняя ошибка | баг | 500 | | `Unavailable` (14) | сервис недоступен | down/перегрузка | 503 | | `DataLoss` (15) | потеря данных | — | 500 | | `Unauthenticated` (16) | нет/невалидный auth | токен | 401 | Различайте: `InvalidArgument` — запрос плох сам по себе (ретрай не поможет); `FailedPrecondition` — система не в том состоянии; `Unavailable` — транзиентно, ретрай уместен; `Aborted` — конфликт, ретрай на верхнем уровне. **Error details** (богатые ошибки). Используются типы из `google.golang.org/genproto/googleapis/rpc/errdetails`: ```go import "google.golang.org/genproto/googleapis/rpc/errdetails" st := status.New(codes.InvalidArgument, "validation failed") st, _ = st.WithDetails( &errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{ {Field: "age", Description: "must be >= 0"}, }}, &errdetails.ErrorInfo{Reason: "BAD_AGE", Domain: "user.v1", Metadata: map[string]string{"got": "-5"}}, ) return nil, st.Err() // Клиент st, _ := status.FromError(err) for _, d := range st.Details() { switch info := d.(type) { case *errdetails.BadRequest: for _, v := range info.GetFieldViolations() { ... } case *errdetails.ErrorInfo: log.Println(info.GetReason(), info.GetDomain()) } } ``` На wire это всё — `google.rpc.Status` (`code`, `message`, `repeated google.protobuf.Any details`), сериализованный в trailer `grpc-status-details-bin`. Анти-паттерн: возвращать «голый» `errors.New("...")` из хендлера — клиент получит `codes.Unknown`. Всегда оборачивайте в `status`. Не утекайте внутренние детали (SQL, стектрейсы) в message публичного API. ### 7. Keepalive, connection management, load balancing **Keepalive** (HTTP/2 PING) детектит мёртвые соединения и держит коннект живым через NAT/LB: ```go // Клиент grpc.WithKeepaliveParams(keepalive.ClientParameters{ Time: 30 * time.Second, // слать PING раз в 30с при простое Timeout: 10 * time.Second, // ждать ответа на PING PermitWithoutStream: true, // пинговать даже без активных RPC }) // Сервер — политика и принудительный refresh коннектов grpc.KeepaliveParams(keepalive.ServerParameters{ MaxConnectionAge: 30 * time.Minute, // принудительно закрывать старые коннекты MaxConnectionAgeGrace: 5 * time.Second, Time: 2 * time.Hour, }) grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ MinTime: 10 * time.Second, // запрет слишком частых PING (иначе GOAWAY) PermitWithoutStream: true, }) ``` `MaxConnectionAge` важен с L4-балансировщиками: gRPC держит долгоживущий коннект, и без принудительного переподключения новые поды бэкенда не получат трафик. **Load balancing — client-side.** gRPC не делает per-request балансировку через L4 LB (один коннект). Подходы: - **Client-side LB**: resolver (DNS/xDS) отдаёт список адресов, балансировщик (`round_robin`, `pick_first`) распределяет RPC по подключениям. ```go conn, _ := grpc.NewClient("dns:///user-service:50051", grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"round_robin":{}}]}`), ) ``` - **Look-aside / proxy LB**: L7-прокси с поддержкой HTTP/2 (Envoy, Linkerd) — балансирует по стримам. - **xDS** (Envoy API) для продвинутого service discovery/LB/canary. **Retry** настраивается через service config (`MethodConfig.retryPolicy`) — backoff, `retryableStatusCodes` (обычно `UNAVAILABLE`). Ретраить безопасно только идемпотентные методы. ## Подводные камни / gotchas - **`context.Background()` вместо входящего ctx** при downstream-вызовах — теряются дедлайн и отмена. Всегда прокидывайте `ctx`. - **Голые ошибки → `codes.Unknown`.** Хендлер должен возвращать `status.Error(...)`, иначе клиент не отличит NotFound от Internal. - **Утечка внутренних деталей** в `message` (SQL, пути, стектрейсы) — кладите их в логи/details, не в публичное сообщение. - **Стрим-ошибка приходит из `Recv()`/`Send()`, а не из вызова метода.** Проверка `err` только на открытии стрима недостаточна. - **`io.EOF` ≠ ошибка.** Это нормальное завершение стрима; реальную ошибку смотрите через `status.FromError`. - **Trailers и статус.** Если читаете заголовки на клиенте до завершения стрима, trailers ещё недоступны. Статус-код всегда в trailers → нужен HTTP/2. - **Большие сообщения.** Дефолтный лимит приёма — 4 МБ (`MaxRecvMsgSize`). Большие payload лучше стримить чанками, а не раздувать одно сообщение. - **Долгоживущие коннекты + L4 LB** = неравномерное распределение; нужен `MaxConnectionAge` или L7-прокси. - **Слишком частый keepalive PING** без согласования с `EnforcementPolicy.MinTime` → сервер шлёт `GOAWAY` с `ENHANCE_YOUR_CALM` и рвёт коннект. - **proto3 и нулевые значения.** Без `optional` нельзя отличить «не задано» от нулевого значения (0, "", false). Для PATCH-семантики используйте `optional` (генерит pointer) или `FieldMask`. - **Изменение wire-контракта.** Нельзя переиспользовать/менять номера полей; удалённые — `reserved`. Иначе ломается совместимость. - **panic в хендлере** без recovery-интерсептора роняет обработку (и потенциально процесс). Recovery обязателен в проде. - **Метаданные регистронезависимы и могут дублироваться** (`[]string`). `md.Get(key)` возвращает срез — не берите слепо `[0]`. - **Дедлайн без `defer cancel()`** — утечка таймера/горутины контекста. ## Вопросы на собеседовании **В:** Почему gRPC требует именно HTTP/2, а не HTTP/1.1? **О:** gRPC опирается на мультиплексирование стримов (много параллельных RPC по одному коннекту без HOL-блокировки на уровне приложения), на потоковую передачу в обе стороны (streaming RPC) и, критически, на **trailers** — статус-код и сообщение gRPC передаются в trailing-заголовках после тела ответа. HTTP/1.1 не поддерживает trailers и мультиплексирование, поэтому полноценный gRPC поверх него невозможен (есть лишь компромисс gRPC-Web). **В:** Чем отличаются 4 типа RPC и как на сервере понять, что стрим завершён? **О:** unary (1→1), server streaming (1→N), client streaming (N→1), bidi (N→M, полный дуплекс). Завершение клиентской стороны стрима сервер видит как `io.EOF` из `Recv()`. Завершение серверного стрима = возврат из метода (на клиенте — `io.EOF` из `Recv()`). В bidi Recv и Send независимы. **В:** Как дедлайн распространяется через цепочку gRPC-вызовов? **О:** Клиент задаёт `context.WithTimeout`; gRPC сериализует оставшееся время в HTTP/2-заголовок `grpc-timeout`. Сервер получает его как deadline в `ctx`. Если сервер делает downstream-вызов, передавая тот же `ctx`, оставшийся бюджет (минус прошедшее время) уходит дальше — дедлайн каскадируется. Истечение даёт `codes.DeadlineExceeded`. Поэтому downstream нельзя вызывать с `context.Background()`. **В:** Чем интерсептор для unary отличается от интерсептора для stream и как пробросить значение в context внутри stream-интерсептора? **О:** unary-интерсептор получает `ctx` и `req` напрямую и оборачивает вызов `handler(ctx, req)`. stream-интерсептор получает `grpc.ServerStream` (без прямого `ctx`) и вызывает `handler(srv, ss)`. Чтобы положить значение в контекст стрима, оборачивают `ServerStream`, переопределяя метод `Context()` (паттерн `wrappedStream`). Цепочки строятся через `ChainUnaryInterceptor`/`ChainStreamInterceptor`, первый аргумент — самый внешний. **В:** Как устроена error model в gRPC? Что не так с возвратом `errors.New`? **О:** Ошибка — это `status.Status`: `codes.Code` + сообщение + опциональные `details` (protobuf-сообщения, на wire — `google.rpc.Status` в trailer `grpc-status-details-bin`). Возврат «голой» ошибки маппится в `codes.Unknown`, теряя семантику. Правильно — `status.Errorf(codes.NotFound, ...)` и `WithDetails(&errdetails.BadRequest{...})` для машиночитаемых деталей; на клиенте — `status.FromError` и обход `st.Details()`. **В:** В чём разница между `InvalidArgument`, `FailedPrecondition`, `Unavailable` и `Aborted` и какие из них имеет смысл ретраить? **О:** `InvalidArgument` — запрос некорректен сам по себе, ретрай бесполезен. `FailedPrecondition` — система не в нужном состоянии (например, ресурс не пуст); ретрай без изменения состояния не поможет. `Unavailable` — транзиентная недоступность, ретрай с backoff уместен. `Aborted` — конфликт конкурентного доступа (optimistic lock); ретрай на уровне всей транзакции/бизнес-операции уместен. Идемпотентность — обязательное условие для безопасного ретрая. **В:** Headers vs trailers в gRPC — зачем оба? **О:** Headers отправляются до тела (метаданные начала ответа, можно до первого `Send` в стриме). Trailers — после тела; в них gRPC передаёт сам статус-код/сообщение и итоговые метаданные (метрики, число строк). В server streaming статус доступен только после всех сообщений — отсюда модель «читать до `io.EOF`, реальная ошибка в последнем `Recv()`». На клиенте header/trailer берут через `grpc.Header(&md)`/`grpc.Trailer(&md)`. **В:** Как gRPC балансирует нагрузку и почему обычный L4-балансировщик плохо подходит? **О:** gRPC держит долгоживущий HTTP/2-коннект и мультиплексирует RPC. L4 LB балансирует на уровне TCP-коннектов, а не запросов — все RPC одного клиента уходят в один бэкенд, новые поды не получают трафик. Решения: client-side LB (resolver + `round_robin`/`pick_first`, опционально xDS), L7-прокси (Envoy/Linkerd) для per-stream балансировки, и `MaxConnectionAge` на сервере для принудительного периодического переподключения. **В:** Когда выбрать gRPC, а когда REST? **О:** gRPC — внутренние сервис-сервис вызовы, где важны производительность (бинарный protobuf, HTTP/2), строгий версионируемый контракт, streaming, низкая латентность, polyglot-кодогенерация. REST/JSON — публичные API, браузерные клиенты, простота отладки (curl), широкая совместимость, кэшируемость по HTTP. Браузер не умеет gRPC напрямую → gRPC-Web (через прокси) или gRPC-Gateway (REST/JSON фасад над теми же proto). ## gRPC vs REST: сравнение | Критерий | gRPC | REST/JSON | |----------|------|-----------| | Транспорт | HTTP/2 (обязательно) | HTTP/1.1, HTTP/2 | | Формат | protobuf (бинарный, компактный) | JSON (текст, человекочитаемый) | | Контракт | строгий, schema-first (.proto), кодоген | OpenAPI/Swagger (опционально) | | Производительность | выше (бинарь, мультиплекс, меньше overhead) | ниже (парсинг JSON, заголовки) | | Streaming | нативно (4 типа) | ограниченно (SSE, chunked, websockets) | | Браузер | напрямую нет → gRPC-Web | нативно | | Отладка | нужны grpcurl/Postman/reflection | curl, браузер | | Совместимость/эволюция | хорошая при дисциплине номеров полей | гибкая, но без гарантий | | Кэширование (HTTP) | нет из коробки | да (GET, ETag, CDN) | | Где применять | внутренние микросервисы, low-latency, polyglot | публичные/браузерные API, интеграции | **Мосты для браузера/REST:** - **gRPC-Web**: ограниченный gRPC из браузера, требует прокси (Envoy/grpcwebproxy), нет client-streaming/bidi в полном объёме. - **gRPC-Gateway**: генерирует reverse-proxy REST/JSON ↔ gRPC из аннотаций в .proto (`google.api.http`) — один контракт, два интерфейса. ## На что копают на senior+ - **HTTP/2 механика**: фреймы, стримы, flow control, HPACK, почему остаётся TCP-level HOL blocking и что меняет HTTP/3/QUIC; как `length-prefixed message` лежит в DATA-фреймах. - **Deadline propagation в распределённой системе**: бюджетирование времени по цепочке, защита от каскадных таймаутов, отличие `Canceled` от `DeadlineExceeded`, что происходит с downstream при отмене. - **Идемпотентность и retry-семантика**: настройка `retryPolicy` в service config, `retryableStatusCodes`, hedging, взаимодействие ретраев с дедлайнами (бюджет на все попытки). - **Backpressure и flow control в стримах**: что если клиент медленно читает; буферизация; почему bidi требует аккуратной горутинной модели (отдельные Recv/Send), дедлоки при синхронном Send в цикле Recv. - **Эволюция схемы**: правила совместимости proto3, `reserved`, `optional`, `oneof`, `FieldMask` для частичных апдейтов, отличие unknown fields поведения. - **Безопасность**: mTLS (`credentials.NewTLS`), per-RPC credentials, ALTS, где валидировать auth (интерсептор vs хендлер), защита от утечки деталей в ошибках. - **Observability**: интеграция с OpenTelemetry (`stats.Handler`), пропагация trace-контекста через метаданные, корреляция request-id, метрики по `codes`. - **Load balancing глубоко**: pick_first vs round_robin, resolver/balancer API, xDS, subsetting, outlier detection, почему `MaxConnectionAge` нужен с L4 LB. - **Производительность**: сжатие (gzip), пулы коннектов vs мультиплексирование, лимиты сообщений, влияние размера сообщений и числа стримов, профилирование сериализации protobuf. - **Сравнение с альтернативами**: gRPC vs GraphQL vs REST vs message queues; когда RPC-модель неуместна (event-driven, pub/sub).