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 - **Protocol Buffers (protobuf)** — бинарный, схемозависимый формат сериализации от Google. Схема описывается в `.proto`, компилируется `protoc` в код целевого языка (для Go — пакет `google.golang.org/protobuf`). - На проводе (wire format) сообщение — это последовательность пар `(tag, value)`, где `tag = (field_number << 3) | wire_type`. **Имена полей на проводе отсутствуют** — кодируются только номера. - Есть 4 актуальных wire type: `0` VARINT, `1` I64 (64-bit), `2` LEN (length-delimited), `5` I32 (32-bit). Типы 3/4 (start/end group) устарели. - **Номера полей 1–15 кодируются в 1 байт** тега, 16–2047 — в 2 байта. Поэтому горячие/частые поля держим в 1–15. - **Совместимость держится на номерах полей, а не на именах**. Backward (новый код читает старые данные) и forward (старый код читает новые данные) совместимость работают за счёт пропуска/сохранения unknown fields. - Главные правила эволюции: **никогда не меняй номер существующего поля**, **никогда не переиспользуй номер удалённого поля** (`reserved`), добавление полей безопасно, переименование безопасно. - В proto3 нет `required`, по умолчанию поля имеют presence "implicit" (нельзя отличить zero от unset) — для явного presence используют `optional`, `oneof` или wrapper-типы. - `oneof`, well-known types (`Timestamp`, `Duration`, `Any`, `Struct`, `FieldMask`, wrappers), `map`, `repeated` + packed encoding — стандартный senior-инструментарий. --- ## Теория ### 1. Что такое protobuf и зачем Protobuf — это IDL (interface definition language) + бинарный формат + кодогенерация. По сравнению с JSON: - Компактнее: нет имён полей, числа кодируются varint'ом. - Быстрее парсится: не нужно парсить текст, нет аллокаций строк-ключей. - Строгая схема и кодогенерация: типобезопасность на этапе компиляции. - Встроенные правила эволюции схемы — основа для долгоживущих API и event-стримов (Kafka и т.п.). Минусы: не human-readable, требует схему для декодирования (без `.proto` поток байт почти бесполезен), слабее в self-describing сценариях. ### 2. proto3: синтаксис, поля, номера, scalar types ```proto syntax = "proto3"; package example.user.v1; option go_package = "github.com/acme/api/gen/user/v1;userv1"; message User { uint64 id = 1; // номер поля (field number / tag) = 1 string name = 2; string email = 3; bool is_active = 4; repeated string roles = 5; } ``` Ключевое: - `= N` — это **номер поля (field number)**, а не значение по умолчанию. Это идентификатор поля на проводе. - Допустимый диапазон номеров: `1 .. 536_870_911` (2^29 − 1). - Зарезервированный диапазон `19000 .. 19999` — для внутренних нужд protobuf, использовать нельзя. Scalar-типы и их соответствие в Go: | proto тип | wire type | Go тип | Заметки | |---|---|---|---| | `int32`, `int64` | VARINT | `int32`, `int64` | неэффективны для отрицательных (10 байт) | | `uint32`, `uint64` | VARINT | `uint32`, `uint64` | | | `sint32`, `sint64` | VARINT | `int32`, `int64` | ZigZag, эффективны для отрицательных | | `bool` | VARINT | `bool` | 1 байт (0/1) | | `enum` | VARINT | свой тип (`int32`) | | | `fixed64`, `sfixed64`, `double` | I64 | `uint64`/`int64`/`float64` | всегда 8 байт | | `fixed32`, `sfixed32`, `float` | I32 | `uint32`/`int32`/`float32` | всегда 4 байта | | `string`, `bytes` | LEN | `string`, `[]byte` | длина + payload; string обязан быть UTF-8 | | `message` | LEN | `*Message` | вложенное сообщение как length-delimited | ### 3. Wire format Сообщение = поток записей. Каждая запись начинается с **тега** (key), за которым следует значение в формате, зависящем от wire type. #### 3.1 Тег: `(field_number << 3) | wire_type` Тег — это varint. Младшие **3 бита** — wire_type, остальные — номер поля. ``` tag = (field_number << 3) | wire_type ``` Wire types: | wire_type | Имя | Используется для | |---|---|---| | 0 | VARINT | int32/64, uint32/64, sint32/64, bool, enum | | 1 | I64 (64-bit) | fixed64, sfixed64, double | | 2 | LEN (length-delimited) | string, bytes, embedded messages, packed repeated | | 3 | SGROUP (deprecated) | start group | | 4 | EGROUP (deprecated) | end group | | 5 | I32 (32-bit) | fixed32, sfixed32, float | Пример: поле `2` типа string. `tag = (2 << 3) | 2 = 0x12`. Дальше идёт varint длины и байты строки. #### 3.2 Varint Varint кодирует целое числом байтов переменной длины. В каждом байте: старший бит (MSB, continuation bit) = 1, если есть следующий байт; 0 — последний. Остальные 7 бит — данные, **little-endian по группам по 7 бит**. Пример: число `300`. - `300 = 0b100101100` - Разбиваем по 7 бит снизу: `0101100` и `0000010`. - Младшая группа идёт первой, ставим continuation: `1010 1100` `0000 0010` = `0xAC 0x02`. ```go // Концептуально (encoding/binary даёт PutUvarint/Uvarint) buf := make([]byte, binary.MaxVarintLen64) n := binary.PutUvarint(buf, 300) // n == 2, buf[:2] == {0xAC, 0x02} ``` Важное следствие: маленькие числа — мало байт. Числа до 127 — 1 байт, до 16383 — 2 байта и т.д. #### 3.3 Почему `int32`/`int64` плохи для отрицательных, и зачем ZigZag Отрицательные `int32`/`int64` интерпретируются как large unsigned (two's complement расширяется до 64 бит), поэтому `-1` занимает **10 байт** varint'а. Чтобы этого избежать, для знаковых, которые часто бывают отрицательными, используют `sint32`/`sint64` с **ZigZag**-кодированием: ``` ZigZag(n) для 32 бит: (n << 1) ^ (n >> 31) ZigZag(n) для 64 бит: (n << 1) ^ (n >> 63) ``` Отображение: `0→0, -1→1, 1→2, -2→3, 2→4, ...`. Небольшие по модулю числа (в т.ч. отрицательные) дают маленький varint. | Значение | int64 (байт) | sint64 / ZigZag (байт) | |---|---|---| | 0 | 1 | 1 | | -1 | 10 | 1 | | 150 | 2 | 2 | | -150 | 10 | 2 | #### 3.4 Почему поля 1–15 дешевле Тег — varint. Поле 1–15 при сдвиге на 3 бита плюс 3 бита wire_type укладывается в 1 байт (значение тега ≤ 127). Поля 16–2047 → тег занимает 2 байта, и т.д. Для часто повторяющихся полей (особенно внутри `repeated` элементов) экономия в 1 байт на каждое значение существенна. ``` field 15, LEN: tag = (15<<3)|2 = 122 = 0x7A → 1 байт field 16, LEN: tag = (16<<3)|2 = 130 → 2 байта (0x82 0x01) ``` #### 3.5 Length-delimited (LEN) Для string/bytes/embedded message/packed repeated: `tag`, затем `varint длина`, затем `длина` байт payload. Embedded message сериализуется рекурсивно и оборачивается как bytes — поэтому неизвестное сообщение можно пропустить, зная длину. ### 4. Совместимость: backward vs forward - **Backward compatibility (обратная)**: **новый** код читает данные, записанные **старой** схемой. Достигается тем, что добавленные поля просто отсутствуют в старых данных → получают default. - **Forward compatibility (прямая)**: **старый** код читает данные, записанные **новой** схемой. Старый код не знает новых полей → они попадают в **unknown fields** и **сохраняются** при ре-сериализации (важно для прокси/relay, которые читают и пишут сообщение, не теряя новых полей). Unknown fields: поля, чьи номера не описаны в текущей схеме. Парсер protobuf не выбрасывает их, а складывает в специальное хранилище и **сериализует обратно**. В Go доступ через рефлексию: ```go m := &userv1.User{} _ = proto.Unmarshal(data, m) unknown := m.ProtoReflect().GetUnknown() // []byte с непрослеженными полями ``` (Замечание: при использовании `Any` или `proto.Merge`/`Clone` unknown fields переносятся; некоторые операции, например JSON-маршалинг, могут их игнорировать.) ### 5. Правила эволюции схемы **Можно безопасно:** - Добавлять новые поля с новыми номерами. - Переименовывать поля (имя не передаётся по проводу) — но это ломает текстовый формат/JSON и исходный код потребителей. - Удалять поля — **но** номер обязательно зарезервировать. - Менять поле между совместимыми varint-типами (см. ниже). - Превращать single-поле в `repeated` того же типа (с оговорками о packed, см. ниже) и наоборот для совместимости чтения. **Нельзя:** - Менять номер существующего поля. - Переиспользовать номер ранее удалённого поля для другого смысла/типа → старые данные будут прочитаны неверно. Используем `reserved`. - Менять wire type поля несовместимым образом (например string ↔ int32). ```proto message User { reserved 3, 7 to 9; // номера запрещены к повторному использованию reserved "email", "phone"; // имена запрещены к переиспользованию uint64 id = 1; string name = 2; } ``` #### Совместимые смены типа - Между собой совместимы (VARINT): `int32`, `int64`, `uint32`, `uint64`, `bool`, `enum`. Можно менять один на другой — но при усечении/переполнении значения интерпретируются как в C (например значение, не влезающее в `int32`, усекается). - `sint32` ↔ `sint64` совместимы между собой, но **не** совместимы с обычными int-типами (другое кодирование, ZigZag). - `string` ↔ `bytes` совместимы, если строка валидный UTF-8 (оба LEN). - `fixed32` ↔ `sfixed32` (оба I32), `fixed64` ↔ `sfixed64` (оба I64). - Embedded message ↔ `bytes` совместимы на wire (оба LEN), если bytes содержит валидную сериализацию. #### proto3: default values и отсутствие required - В proto3 нет `required` (был в proto2 и считается анти-паттерном: required ломает эволюцию — нельзя удалить required-поле, не сломав старых читателей). - Default по типам: числа → `0`, `bool` → `false`, string → `""`, bytes → пусто, enum → `0`, message → `nil` (unset). - По умолчанию (implicit presence) **поля со значением default не сериализуются** на провод (нулевые int, пустые string и т.п. пропускаются). Это и даёт компактность, но делает невозможным отличить «явно 0» от «не задано». ### 6. Presence и proto3 `optional` Implicit presence (по умолчанию в proto3 для scalar): нельзя узнать, было ли поле явно установлено в zero или вообще отсутствовало. `optional` добавляет **explicit presence** — генерирует методы `Has...()` и pointer-семантику в Go: ```proto message UpdateUserRequest { uint64 id = 1; optional string name = 2; // explicit presence optional bool is_active = 3; } ``` ```go req := &userv1.UpdateUserRequest{} if req.Name != nil { // в Go optional scalar → *string name := req.GetName() _ = name } // req.HasName() также доступен через рефлексию / сгенерированный код ``` Под капотом `optional` реализован как `oneof` из одного поля (synthetic oneof) — на проводе ничего особенного, presence-бит хранится локально. Message-поля **всегда** имеют explicit presence (nil = unset), `optional` для них избыточен. Альтернатива до появления proto3 `optional` — wrapper-типы (см. well-known types). ### 7. oneof `oneof` — набор полей, из которых установлено **не более одного**. Установка одного автоматически сбрасывает остальные. На проводе oneof — это просто обычные поля; «oneof-ность» обеспечивается генератором/рантаймом (при чтении нескольких полей одного oneof побеждает последнее в потоке). ```proto message Event { string id = 1; oneof payload { UserCreated user_created = 10; UserDeleted user_deleted = 11; OrderPlaced order_placed = 12; } } ``` В Go генерируется интерфейс-обёртка: ```go switch p := ev.GetPayload().(type) { case *eventv1.Event_UserCreated: handleCreated(p.UserCreated) case *eventv1.Event_UserDeleted: handleDeleted(p.UserDeleted) case *eventv1.Event_OrderPlaced: handlePlaced(p.OrderPlaced) case nil: // ничего не установлено } ``` Эволюция и подводные камни oneof: - Добавлять новые варианты в oneof **безопасно по wire** (это новый номер поля). Но старый код, не знающий нового варианта, увидит oneof как «не установлен» (вариант попадёт в unknown fields). - **Нельзя** переносить существующее обычное поле внутрь oneof или наоборот безопасно во всех языках: это меняет семантику presence, хотя wire совместим. Перемещение **одного** поля в/из oneof обычно бинарно совместимо; перемещение нескольких — нет. - `oneof` не может быть `repeated`. - Поля внутри oneof не могут быть `optional` (presence у oneof уже явный). ### 8. Well-known types Импортируются из `google/protobuf/*.proto`, в Go — пакет `google.golang.org/protobuf/types/known/...`. | Тип | Назначение | |---|---| | `Timestamp` | момент времени (seconds + nanos от Unix epoch, UTC) | | `Duration` | длительность (seconds + nanos, знаковая) | | `Empty` | пустое сообщение (для RPC без аргументов/ответа) | | `Any` | произвольное упакованное сообщение + type URL | | `Struct` / `Value` / `ListValue` | динамический JSON-подобный объект | | `FieldMask` | список путей полей (для partial update / projection) | | `BoolValue`, `Int32Value`, `StringValue`, ... (wrappers) | scalar с explicit presence | ```proto import "google/protobuf/timestamp.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/field_mask.proto"; import "google/protobuf/wrappers.proto"; import "google/protobuf/any.proto"; message Job { google.protobuf.Timestamp created_at = 1; google.protobuf.Duration timeout = 2; google.protobuf.Int32Value retries = 3; // позволяет отличить 0 от "не задано" google.protobuf.FieldMask update_mask = 4; google.protobuf.Any payload = 5; } ``` ```go import ( "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/anypb" ) job.CreatedAt = timestamppb.New(time.Now()) t := job.GetCreatedAt().AsTime() // Any: упаковка/распаковка конкретного типа anyMsg, _ := anypb.New(&userv1.User{Id: 1}) var u userv1.User _ = anyMsg.UnmarshalTo(&u) ``` **Wrappers и presence**: до proto3 `optional` единственным способом отличить «zero» от «unset» для scalar были wrapper-сообщения (message → explicit presence, nil = unset). Сейчас для большинства случаев предпочтительнее `optional`, но wrappers всё ещё встречаются в legacy API и при работе с JSON-маппингом. **FieldMask**: применяется в partial-update API (`UpdateUserRequest { User user; FieldMask update_mask; }`) — сервер обновляет только перечисленные в маске пути. Решает проблему «как отличить, что клиент хочет занулить поле, а что — не трогать». **Any**: хранит сериализованное сообщение + `type_url` (например `type.googleapis.com/example.user.v1.User`). Опасность — теряется статическая типизация, нужен реестр типов для распаковки. ### 9. enum ```proto enum Status { STATUS_UNSPECIFIED = 0; // ОБЯЗАТЕЛЬНО первый и = 0 STATUS_ACTIVE = 1; STATUS_SUSPENDED = 2; reserved 3, 4; reserved "STATUS_LEGACY"; } ``` - В proto3 **первый элемент обязан иметь значение 0** и является default. Конвенция: `*_UNSPECIFIED = 0` — чтобы default не означал случайно валидный бизнес-статус. - enum кодируется как VARINT (int32). - **Open enum semantics** (proto3): неизвестное число при чтении **сохраняется** (не падает), доступно через `GetXxx()` как сырое int-значение. Это даёт forward-совместимость: добавление новых значений enum безопасно. (В proto2 enum closed — неизвестное значение уходит в unknown fields.) - Эволюция: добавлять значения можно; удалять — резервируй номер и имя через `reserved`. Не переиспользуй удалённые номера. - Allow aliases: `option allow_alias = true;` разрешает два имени с одинаковым номером. ### 10. repeated, map, packed encoding ```proto message Metrics { repeated int32 samples = 1; // packed по умолчанию (scalar) repeated string tags = 2; // НЕ packed (LEN-элементы) map counters = 3; } ``` **Packed encoding**: в proto3 `repeated` для scalar-типов с фиксированным/varint представлением (числа, bool, enum) кодируется **packed по умолчанию** — один тег + одна length-delimited область со всеми значениями подряд, без повторения тега: ``` [tag(LEN)] [len] [val1][val2][val3]... ``` Это экономит по 1 тегу на элемент. `repeated string`/`bytes`/`message` **не** packable (каждый элемент — отдельная LEN-запись со своим тегом). Совместимость: парсер обязан принимать **обе** формы (packed и unpacked) для packable полей — поэтому смена `[packed=true]`/`[packed=false]` бинарно совместима. **map** — синтаксический сахар. На проводе `map` эквивалентен: ```proto message MapEntry { K key = 1; V value = 2; } repeated MapEntry counters = 3; ``` Следствия: - map не сохраняет порядок, дубликаты ключей → побеждает последний. - map нельзя сделать `repeated`, ключ — только integral/string/bool, значение — любой тип кроме другого map. - Можно эволюционировать `map` ↔ соответствующий `repeated MapEntry` бинарно совместимо. --- ## Подводные камни / gotchas - **Перепутать field number и значение по умолчанию.** `int32 x = 5;` — это поле номер 5, а не «x = 5». - **Переиспользование номера удалённого поля** — самая опасная ошибка эволюции: старые сериализованные данные молча декодируются в новое поле с тем же номером → порча данных. Всегда `reserved`. - **Implicit presence в proto3**: нельзя отличить «0/пустую строку/false» от «не задано». Для PATCH-семантики используй `optional`, wrappers или `FieldMask`. - **Отрицательные `int32`/`int64` занимают 10 байт.** Для координат, дельт и т.п. бери `sint32`/`sint64`. - **Смена `int*` ↔ `sint*` несовместима** (разное кодирование), хотя оба VARINT. - **enum default = 0**: если 0 — валидный бизнес-смысл, легко получить молчаливый баг при unset. Держи `UNSPECIFIED = 0`. - **proto3 open enum**: неизвестное значение enum приходит как сырое число; switch без `default` это пропустит. Всегда обрабатывай unknown. - **Unknown fields могут теряться** при некоторых операциях (JSON-маршалинг по умолчанию их игнорирует; устаревшие API). Для прокси, не теряющих новые поля, проверь, что path read→write идёт через бинарный proto. - **`Any` ломает статическую типизацию** и требует реестра типов; легко получить runtime-ошибку распаковки. - **map не детерминирован**: для стабильной сериализации (подписи, хеши) protobuf **не гарантирует** канонический порядок. Не используй обычную сериализацию для криптоподписи без deterministic-режима (`proto.MarshalOptions{Deterministic: true}` сортирует ключи map, но это не межъязыковая гарантия канона). - **proto2 vs proto3 enum**: closed vs open — при миксе версий поведение неизвестного значения разное. - **Большие номера полей (>15, >2047)** удорожают тег; не трать 1–15 на редкие поля. - **`required` (proto2)** делает поле невозможным к удалению — поэтому в proto3 его убрали; не воскрешай эту семантику ручными проверками так, чтобы они ломали forward-совместимость. --- ## Вопросы на собеседовании **В:** Как кодируется тег поля на проводе и из чего он состоит? **О:** Тег — это varint, равный `(field_number << 3) | wire_type`. Младшие 3 бита — wire type (0 VARINT, 1 I64, 2 LEN, 5 I32), старшие — номер поля. Имя поля на проводе не передаётся вообще. **В:** Почему номера полей 1–15 предпочтительны для часто используемых полей? **О:** Тег кодируется varint'ом. Для номеров 1–15 значение тега ≤ 127 и помещается в 1 байт; для 16–2047 — 2 байта. На повторяющихся/частых полях экономия 1 байт на каждое значение заметна, поэтому горячие поля держат в 1–15. **В:** Чем отличается backward от forward совместимости в protobuf и за счёт чего они достигаются? **О:** Backward — новый код читает старые данные (новые поля просто отсутствуют → default). Forward — старый код читает новые данные (неизвестные поля попадают в unknown fields и сохраняются при ре-сериализации). Оба держатся на стабильности номеров полей и на сохранении unknown fields. **В:** Какие изменения схемы безопасны, а какие нет? **О:** Безопасно: добавлять поля с новыми номерами, переименовывать поля (имя не на проводе), удалять поля с `reserved` номера. Совместимы смены между varint-типами (int32/int64/uint32/uint64/bool/enum), string↔bytes, fixed32↔sfixed32. Небезопасно: менять номер существующего поля, переиспользовать удалённый номер, менять wire type несовместимо (string↔int32, int↔sint). **В:** Почему `-1` в поле `int64` занимает 10 байт и как это исправить? **О:** Отрицательные значения int32/int64 представляются в two's complement, расширенном до 64 бит, поэтому старшие биты выставлены и varint занимает максимум — 10 байт. Решение: `sint32`/`sint64` с ZigZag-кодированием (`(n<<1) ^ (n>>63)`), где небольшие по модулю отрицательные числа дают короткий varint. **В:** В proto3 нет `required` и по умолчанию нельзя отличить 0 от unset. Как реализовать PATCH/partial update? **О:** Варианты: (1) `optional` поля — дают explicit presence и `Has`-методы/pointer в Go; (2) wrapper-типы (`Int32Value` и т.п.) — message с nil = unset; (3) `FieldMask` — клиент явно перечисляет, какие пути обновлять. FieldMask лучше всего решает «не трогать vs занулить». **В:** Что такое `oneof`, как он ведёт себя на проводе и какие риски при эволюции? **О:** `oneof` гарантирует, что установлено не более одного поля из группы; установка одного сбрасывает остальные. На проводе это обычные поля с разными номерами, при чтении нескольких побеждает последнее. Добавлять варианты безопасно (новый номер), но старый код увидит новый вариант как «не установлен». Перенос нескольких полей в/из oneof небезопасен; одно поле обычно бинарно совместимо. Поля oneof не могут быть repeated/optional. **В:** Как packed encoding работает для `repeated` и для каких типов применяется? **О:** В proto3 `repeated` scalar (числа/bool/enum) по умолчанию packed: один тег + length-delimited блок со всеми значениями подряд, без повторения тега на элемент. `repeated string/bytes/message` не packable — каждый элемент отдельная LEN-запись. Парсер обязан принимать обе формы, поэтому переключение packed бинарно совместимо. **В:** Почему первый элемент enum в proto3 обязан быть 0 и что такое open enum? **О:** Значение 0 — это default для unset, и proto3 требует, чтобы оно было первым; конвенция `*_UNSPECIFIED = 0` не даёт default'у означать валидный бизнес-статус. Open enum (proto3): неизвестное при чтении число сохраняется как сырое значение (не падает и не уходит в unknown), что обеспечивает forward-совместимость при добавлении новых значений. **В:** Как `map` представлен на проводе и какие из этого следствия? **О:** map — сахар над `repeated` сообщением `{key=1; value=2;}`. Следствия: порядок не сохраняется, при дублях ключей побеждает последний, ключ только integral/string/bool, значение — не map, нельзя repeated map. Сериализация map не канонична по порядку — для подписей нужен deterministic-режим. --- ## На что копают на senior+ - **Точный wire format на байтах.** Попросят руками закодировать сообщение: посчитать тег `(field<<3)|type`, разложить число в varint, объяснить continuation bit и little-endian по 7 бит, посчитать байты для packed repeated. - **Эволюция в проде с историческими данными.** Сценарий: «удалили поле 3, через год добавили новое поле под номером 3» — что произойдёт со старыми событиями в Kafka/БД. Ожидают: молчаливая порча, отсюда дисциплина `reserved` и code review схем, схема-реестр (buf, schema registry), линтеры (buf lint/breaking). - **Presence-модель целиком.** Различие implicit/explicit presence, как `optional` реализован через synthetic oneof, почему message-поля всегда имеют presence, когда wrappers vs optional vs FieldMask, влияние на JSON-маппинг (`emitUnpopulated`, `useProtoNames`). - **Unknown fields в роли relay.** Прокси/шлюз читает сообщение и пишет дальше — гарантия не потерять новые поля. Где unknown теряются (JSON, `Any`, некоторые трансформации), и как это проверить. - **Детерминированная сериализация и её пределы.** `proto.MarshalOptions{Deterministic: true}` сортирует map-ключи, но protobuf официально **не** гарантирует канонический byte-output между версиями/языками — поэтому нельзя строить криптоподписи на «просто Marshal». Альтернативы и риски. - **Группы и legacy.** Знание, что wire type 3/4 (groups) deprecated, чем заменены (nested messages), и почему встречаются в старых схемах. - **`Any` vs `oneof` vs полиморфизм.** Когда `Any` оправдан (плагины, неизвестные заранее типы) против `oneof` (закрытый набор, типобезопасность), стоимость type URL и реестра. - **Инструментарий.** buf (lint, breaking change detection, BSR), `protoc` плагины, `protoreflect`/dynamicpb для обобщённой обработки, gRPC reflection, влияние `go_package` и versioned packages (`v1`, `v1beta1`) на эволюцию API. - **Производительность и аллокации в Go.** Стоимость парсинга, переиспользование сообщений, `proto.Reset`, влияние repeated/map на GC, почему `string` zero-copy не всегда возможен.