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/ > Модуль: Core Go · Уровень: Senior+ ## TL;DR Дженерики появились в Go 1.18 и позволяют параметризовать функции и типы по типам (type parameters) с ограничениями (constraints), которые задаются интерфейсами, описывающими **множество типов** (type set), а не множество методов. Под капотом Go использует не чистую мономорфизацию (как C++/Rust) и не чисто словари (как Java erasure), а гибрид — **GC Shape Stenciling**: компилятор генерирует по одной инстанцированной копии кода на каждую уникальную «GC-форму» (gcshape) аргумента, а различия в пределах одной формы разрешаются через скрытый словарь (dictionary). Это даёт компактный бинарник, но и неочевидные накладные расходы: дженерики далеко не всегда быстрее интерфейсов и часто теряют девиртуализацию и инлайнинг. ## Теория ### Базовый синтаксис: type parameters Параметр типа объявляется в квадратных скобках после имени функции/типа, перед обычным списком параметров: ```go func Map[T, U any](s []T, f func(T) U) []U { r := make([]U, len(s)) for i, v := range s { r[i] = f(v) } return r } type Stack[T any] struct { items []T } func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) } ``` Важные правила синтаксиса: - `[T any]` — `T` это параметр типа, `any` — его ограничение (constraint). `any` это псевдоним для `interface{}`. - Параметр типа имеет область видимости всей сигнатуры и тела, а у методов — берётся из объявления типа-ресивера. Метод **не может** вводить собственные параметры типа (см. ограничения). - Инстанцирование (instantiation) — подстановка конкретных типов: `Map[int, string](...)`. Часто аргументы типов выводятся (type inference), и скобки опускаются. ### Constraints как интерфейсы — но это интерфейсы про множества типов Constraint — это интерфейс. Но в Go 1.18 интерфейсы получили расширенный смысл. Теперь интерфейс описывает **type set** — множество типов, которые ему удовлетворяют. Обычный интерфейс с методами — это множество всех типов, реализующих эти методы. Новые элементы (union, approximation) расширяют, что можно записать в интерфейс. ```go type Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } ``` Ключевое разграничение, которое любят спрашивать: - **Basic interface** — содержит только методы (и встроенные basic-интерфейсы). Может использоваться и как тип значения, и как constraint. - **General interface** (с union/approximation/`comparable`) — может использоваться **только как constraint**, его нельзя применять как тип переменной. Попытка `var x Ordered` — ошибка компиляции: «interface contains type constraints». ### Approximation: элемент `~` `~T` означает «все типы, у которых **underlying type** равен `T`». Это критично для пользовательских типов: ```go type Celsius float64 func Sum[T ~float64](xs []T) T { /* ... */ } // принимает и Celsius func Sum2[T float64](xs []T) T { /* ... */ } // принимает ТОЛЬКО float64, Celsius — нет ``` - `~int` = `int` + любой `type MyInt int`. - Тип в `~T` обязан сам быть своим underlying (нельзя `~MyInt`, если `MyInt` имеет underlying `int`; нельзя `~error` — интерфейс не может стоять под `~`). - Без `~` constraint требует **точного** совпадения типа, что почти всегда не то, что нужно для числовых дженериков. ### Union элементы Union перечисляет альтернативы через `|`. Type set union — это объединение type set'ов слагаемых: ```go type Number interface { ~int | ~int64 | ~float64 } ``` Ограничения union: - Терм union не может содержать методов (нельзя `int | io.Reader`), кроме одиночного интерфейса без методов как единственного элемента в особых случаях. Практически: union — это про конкретные/approximated типы. - Term'ы не должны пересекаться по type set, если используется `~` некорректно (компилятор это проверяет в части случаев). - В теле функции с union-constraint можно использовать только те операции, что **поддерживаются всеми** членами множества. Если `Number` содержит и `string`, и `int`, оператор `+` разрешён (оба поддерживают), а `&` (битовый) — уже нет, т.к. `string` его не поддерживает. ### Type sets — модель, лежащая в основе Понятие type set — это формальная семантика constraint'ов. Правила: - Метод `m()` в интерфейсе сужает множество до типов с этим методом. - `T` (тип) — множество `{T}`. - `~T` — множество всех типов с underlying `T`. - `A | B` — объединение. - Несколько элементов на разных строках интерфейса — **пересечение** (intersection). ```go type SignedInt interface { ~int | ~int32 | ~int64 } type Stringer interface { String() string } // Пересечение: типы с underlying int* И методом String() type Both interface { SignedInt Stringer } ``` Операция в теле дженерика легальна, только если её поддерживает **каждый** тип из type set'а. Это даёт статическую проверку без рантайм-сюрпризов. ### `comparable` и его особенности (важно для 1.20+) `comparable` — предопределённый constraint: множество всех типов, которые можно сравнивать через `==` и `!=` без паники в рантайме на уровне типа. Используется прежде всего для ключей map: ```go func Keys[K comparable, V any](m map[K]V) []K { /* ... */ } ``` Тонкость, которую обязательно копают на senior: - До Go 1.20 `comparable` означал строго сравнимые типы. Интерфейсные типы (например `any`) **не удовлетворяли** `comparable`, потому что сравнение интерфейсов может паниковать в рантайме, если внутри лежит несравнимый тип (slice, map, func). - В **Go 1.20** правила смягчили: теперь `comparable` удовлетворяют и «строго сравнимые», и обычные интерфейсные типы (так называемые «spec-comparable», которые сравнимы синтаксически, но могут паниковать в рантайме). То есть теперь `any` удовлетворяет `comparable`. ```go // До 1.20 — ошибка компиляции, начиная с 1.20 — компилируется: func f[T comparable]() {} var _ = f[any] ``` - Следствие/риск: код, который раньше был защищён компилятором от рантайм-паники сравнения, теперь может скомпилироваться и **паниковать в рантайме**, если в `any`-ключ положить slice/map/func: ```go m := map[any]int{} m[[]int{1}] = 1 // паника: runtime error: hash of unhashable type []int ``` То есть `comparable` после 1.20 гарантирует синтаксическую сравнимость, но не гарантирует отсутствие рантайм-паники для интерфейсных типов. ### Инстанцирование под капотом: GC Shape Stenciling Это центральная senior-тема. Существует два классических подхода к реализации дженериков: | Подход | Кто использует | Плюсы | Минусы | |---|---|---|---| | Мономорфизация (stenciling) | C++ templates, Rust | максимум скорости, инлайнинг, специализация | раздувание кода (code bloat), долгая компиляция | | Словари / type erasure | Java (erasure), C# (частично) | компактный код, одна копия | боксинг, динамические диспетчи, медленнее | Go выбрал **гибрид** — **GC Shape Stenciling**. Идея: 1. Компилятор генерирует не по копии на каждый конкретный тип, а **по копии на каждую уникальную GC-форму (gcshape)** аргумента типа. 2. **GC shape** типа — это, грубо, то, как тип выглядит для аллокатора и сборщика мусора: его размер, выравнивание и расположение указателей внутри (pointer/scalar map). Главное правило: **все типы-указатели имеют одну и ту же GC shape** (`*T` для любого `T` — это просто слово-указатель). Поэтому `Stack[*Foo]`, `Stack[*Bar]`, `Stack[*Baz]` используют **одну** инстанцированную копию кода. 3. Типы с разной формой получают разные копии: `int` (скаляр, 8 байт), `string` (16 байт, содержит указатель), `[3]int` и т.д. — у каждого своя стенцилированная копия. Но одной формы недостаточно: внутри функции могут понадобиться сведения, специфичные для **конкретного** типа, а не его формы — например: - какой именно `*runtime._type` (дескриптор типа) использовать при `make`, конверсиях, рефлексии; - адреса методов конкретного типа, если constraint содержит методы (нужно вызвать `T.String()`); - метаданные для itab при упаковке в интерфейс. Эту специфику компилятор передаёт через скрытый **dictionary (словарь)** — дополнительный неявный аргумент, который добавляется к каждой инстанцированной функции. Словарь содержит: - указатели на runtime type descriptors для каждого параметра типа и производных типов; - itab'ы (таблицы методов) для вызовов методов из constraint'ов и для конверсий в интерфейсы; - информацию о суб-словарях для вложенных вызовов других дженериков. ```text Вызов: Sum[Celsius](xs) ↓ Линкуется в копию для gcshape "float64-подобный скаляр 8 байт" ↓ Передаётся скрытый dict с *_type для Celsius и т.п. ``` Таким образом: - **Stenciling** работает по форме (мало копий) → компактный бинарник, разумное время компиляции. - **Dictionaries** закрывают разницу между типами одной формы → корректность без отдельной копии на каждый тип. #### Почему это влияет на производительность Это самая частая ошибка интуиции: «дженерики = мономорфизация = быстро». В Go это не так. 1. **Указательные типы делят код и работают через словарь.** Для `Stack[*Foo]` обращение к информации о типе идёт через индирекцию в словарь, а не зашито константой. Вызовы методов из constraint'а становятся **косвенными** (загрузка itab из словаря, потом вызов) — фактически как у интерфейсов, devirtualization теряется. 2. **Потеря инлайнинга.** Поскольку одна копия обслуживает много типов, компилятору труднее инлайнить и специализировать арифметику/вызовы; часть оптимизаций, доступных для конкретного типа, недоступна. 3. **Накладные на словарь.** Дополнительный аргумент-словарь, лишние загрузки из памяти. 4. **Результат:** дженерик-код над указательными типами и с вызовами методов может быть **не быстрее, а иногда медленнее**, чем эквивалент с интерфейсами — и заметно медленнее, чем рукописная мономорфная версия. Зато над скалярными типами (числа) дженерики обычно выигрывают у интерфейсов, потому что избегают боксинга (heap-аллокаций при упаковке значения в интерфейс). Практический вывод: для скаляров (числа) дженерики дают выигрыш за счёт отсутствия боксинга; для указательных типов с вызовами методов выигрыша по сравнению с интерфейсами может не быть. ### Когда дженерики уместны - **Контейнеры/структуры данных**: типобезопасные стеки, очереди, деревья, sets, lru-кэши — раньше делались через `interface{}` с приведениями. - **Алгоритмы над коллекциями**: `Map`, `Filter`, `Reduce`, сортировки, бинпоиск. - **Стандартные пакеты**: `slices` и `maps` (стабилизированы в Go 1.21), `cmp`, частично `sync` (`sync.OnceValue` и т.п.). `golang.org/x/exp/constraints` даёт `constraints.Ordered`, `Integer`, `Signed`, `Float`. - Функции, где раньше копипастили версии под каждый числовой тип. Антипаттерны: не стоит «дженерифицировать» ради дженериков. Если у вас один-два конкретных типа — обычные функции/интерфейсы проще и часто быстрее. Дженерики для слоёв с поведением (обычные методы, полиморфизм через поведение) — интерфейсы лучше. ### Ограничения языка (важно знать наизусть) - **Нет параметризованных методов.** Метод не может объявлять собственные type-параметры: `func (r Repo) Get[T any]() T` — запрещено. Type-параметры методов могут приходить **только** от типа-ресивера. - **Нет специализации (template specialization).** Нельзя написать особую реализацию `Max` для `string` и другую для `int` — одна реализация на всё множество. - **Нет методов в union с другими типами** — нельзя смешивать method-элементы и type-элементы в одном union произвольно. - **Ограничения type inference.** Вывод типов не всемогущ: не выводит из возвращаемого значения, плохо работает через несколько уровней, не всегда выводит из constraint'ов. Иногда приходится указывать аргументы типа явно. - **Нет дженериков на уровне пакетов/переменных** в смысле «дженерик-переменная». - **Нельзя использовать general interface как тип значения** (`var x comparable`, `var x Ordered` — ошибка). - **Нет ковариантности/контравариантности**: `[]Stack[Animal]` не совместим с `[]Stack[Dog]`. - **Type switch по параметру типа `T`** напрямую не разрешён (`switch T {}` нельзя); приходится свитчить по значению `any(v)`. ## Подводные камни / gotchas - **`comparable` теперь пускает `any` (1.20+) → рантайм-паника.** Компилятор больше не страхует: `map[any]V` с несравнимым ключом упадёт в рантайме. Если нужна строгая гарантия — придётся проектировать API иначе или валидировать. - **Забыли `~` в constraint.** `func F[T int]` не примет ваш `type ID int`. Почти всегда нужно `~int`. Очень частая ошибка. - **General interface как обычный тип.** ```go type Ordered interface { ~int | ~string } var x Ordered // ошибка компиляции: interface contains type constraints ``` - **Ожидание мономорфизации и скорости.** Бенчат «дженерик vs интерфейс» на указательных типах и удивляются, что дженерик не быстрее — это следствие GC Shape Stenciling + словарей + потери девиртуализации. - **Операции, не поддерживаемые всем type set'ом.** Если constraint включает `string`, нельзя применять побитовые операции, даже если на практике вызывают только с `int`. Компилятор смотрит на всё множество, а не на конкретный вызов. - **Type inference и nil/литералы.** `Map(s, func(x) {...})` иногда не выводит тип результата; нужно `Map[int, string](...)`. Литерал `nil` без типа нередко ломает вывод. - **Метод не может стать дженериком.** Желание сделать «дженерик-метод» на не-дженерик типе упирается в стену; решается выносом в свободную функцию. - **Zero value параметра типа.** Чтобы получить нулевое значение `T`, используют `var zero T` (нельзя `T{}` в общем случае, нельзя `nil`, т.к. `T` может быть не-nullable). - **Сравнение значений `T` требует `comparable`.** Внутри `[T any]` нельзя писать `a == b`; нужно `[T comparable]`. - **Конверсия в интерфейс внутри дженерика аллоцирует.** `any(v)` для скалярного `T` боксит значение — теряется часть выигрыша. ## Вопросы на собеседовании **В:** Что такое type set и почему интерфейс в Go 1.18 — это «множество типов»? **О:** Интерфейс теперь формально определяет множество типов, которые ему удовлетворяют. Для basic-интерфейса это все типы с нужными методами. Union (`A | B`) даёт объединение множеств, approximation (`~T`) — все типы с underlying `T`, перечисление элементов на разных строках — пересечение. Операция в теле дженерика легальна, только если её поддерживает каждый тип из множества. Это позволяет статически проверять корректность без рантайм-проверок. **В:** Чем `~int` отличается от `int` в constraint? **О:** `int` требует точного совпадения типа — пользовательский `type ID int` не подойдёт. `~int` означает «все типы с underlying `int`», включая `ID`. Под `~` можно ставить только тип, который сам является своим underlying, и нельзя ставить интерфейс. **В:** Как реализованы дженерики в Go под капотом? Это мономорфизация? **О:** Нет, это гибрид — GC Shape Stenciling. Компилятор генерирует по копии кода не на каждый конкретный тип, а на каждую уникальную GC-форму (gcshape): размер, выравнивание, расположение указателей. Все указательные типы имеют одну форму, поэтому делят одну копию. Различия конкретных типов внутри одной формы передаются через скрытый словарь (dictionary): runtime type descriptors, itab'ы для вызовов методов, суб-словари. Это компромисс между размером бинарника/скоростью компиляции (как у словарей) и производительностью (как у мономорфизации). **В:** Почему дженерики не всегда быстрее интерфейсов? **О:** Из-за словарей. Для типов одной GC-формы (особенно указателей) вызовы методов из constraint'а идут косвенно через itab в словаре — это та же динамическая диспетчеризация, что у интерфейсов, девиртуализация теряется. Плюс затрудняется инлайнинг и специализация, есть накладные на сам словарь-аргумент. Выигрыш дженерики дают в основном на скалярных типах за счёт устранения боксинга, которого требует интерфейс. **В:** Что изменилось в `comparable` в Go 1.20 и какие риски это создаёт? **О:** До 1.20 `comparable` означал строго сравнимые типы и интерфейсы (включая `any`) ему не удовлетворяли, т.к. сравнение интерфейсов может паниковать в рантайме. С 1.20 правила смягчены: `comparable` удовлетворяют и обычные интерфейсные типы. Теперь `f[any]` компилируется. Риск — компилятор больше не защищает от рантайм-паники: `map[any]int` с ключом-slice упадёт в рантайме `hash of unhashable type`. **В:** Почему `Stack[*A]` и `Stack[*B]` используют один экземпляр кода, а `Stack[int]` и `Stack[string]` — разные? **О:** Потому что `*A` и `*B` имеют идентичную GC shape (одно слово-указатель, одинаковый pointer map), а `int` (скаляр 8 байт без указателей) и `string` (16 байт с указателем внутри) — разные формы. Stenciling идёт по форме, а различие между `*A` и `*B` (их type descriptors, методы) закрывается словарём. **В:** Какие принципиальные ограничения есть у дженериков Go? **О:** Нельзя объявлять параметризованные методы (type-параметры метода берутся только от ресивера); нет специализации под конкретный тип; general-интерфейсы (с union/`comparable`) нельзя использовать как тип значения; type inference ограничен (не выводит из возвращаемого значения, не всегда через несколько уровней); нет ковариантности; нельзя делать type switch напрямую по `T`. **В:** Когда стоит использовать дженерики, а когда интерфейсы? **О:** Дженерики — для типобезопасных контейнеров и алгоритмов над коллекциями (`slices`, `maps`, `Map/Filter/Reduce`), где раньше был `interface{}` с приведениями, особенно для скаляров (избегаем боксинга). Интерфейсы — когда нужен полиморфизм по поведению, разные реализации одного контракта, плагинная архитектура. Если типов один-два — обычные функции проще и часто быстрее, чем дженерики. **В:** Что такое словарь (dictionary) и что в нём лежит? **О:** Это скрытый дополнительный аргумент инстанцированной дженерик-функции. Содержит runtime type descriptors (`*_type`) для параметров типа и производных типов, itab'ы для вызовов методов из constraint'ов и для конверсий в интерфейсы, а также суб-словари для вложенных дженерик-вызовов. Через него код, общий для одной GC-формы, получает доступ к специфике конкретного типа. ## На что копают на senior+ - **Глубина по GC Shape Stenciling.** Senior должен не просто знать слово «stenciling», а объяснить: что такое gcshape (размер/выравнивание/pointer map), почему все указатели имеют одну форму, что именно лежит в словаре, и как это связано с потерей девиртуализации. Follow-up: «почему Go не сделал полную мономорфизацию?» — ответ про code bloat и время компиляции, осознанный инженерный компромисс. - **Бенчмарк-интуиция.** Спросят: «дженерик-Max над `[]int` vs интерфейс-версия — что быстрее и почему?» Сильный ответ: дженерик быстрее на скалярах (нет боксинга), но над указательными типами с вызовами методов разница может исчезнуть из-за индирекции через словарь. Ещё лучше — упомянуть, что надо мерить `go test -bench` и смотреть escape analysis / аллокации. - **Эволюция `comparable`.** Различение «strictly comparable» vs «spec-comparable» и понимание, что 1.20 убрал статическую защиту и переложил риск в рантайм — это маркер senior. Follow-up: как спроектировать API так, чтобы не допустить несравнимый ключ. - **Границы type inference.** Покажут код, который не компилируется без явных аргументов типа, и спросят почему. Нужно знать, что вывод не идёт от возвращаемого типа и ограниченно работает через цепочки вызовов. - **Понимание, чего дженерики НЕ решают.** Нет параметризованных методов → нельзя сделать дженерик-интерфейс с дженерик-методом; нет ковариантности → проблемы с коллекциями параметризованных типов. Senior называет это сразу и предлагает обходные пути (свободные функции, type-параметры на уровне типа). - **Связь с runtime и escape analysis.** Понимание, что конверсия `any(v)` внутри дженерика боксит и аллоцирует, что словарь — это память и индирекция, и как это видно в профиле/дизассемблере (`go build -gcflags=-m`, `go tool objdump`).