Стандартная библиотека и инструментарий: пакет os, make/new, линтеры, отладка, кодогенерация
Кратко о теме
Заголовок раздела «Кратко о теме»Эта подтема — про «повседневный Go»: что даёт стандартная библиотека, какими встроенными функциями создаются объекты, чем инструментальная цепочка (go build, go vet, gofmt, линтеры, dlv, pprof, go generate) отличается от языка как такового. Вопросы здесь редко бывают глубокими по отдельности, зато их много и они широкие: половина проверяет, помните ли вы сигнатуры (os.Getwd, os.Symlink, os.Chmod), а вторая половина — есть ли у вас цельная модель языка (ООП ли Go, что вместо наследования, где живут объекты, как планировщик соотносится с потоками ОС).
Ключевая модель для «встроенных функций»: в Go есть небольшой набор builtin-ов (make, new, len, cap, append, copy, delete, panic, recover, close, min, max, clear), которые не лежат ни в каком импортируемом пакете — они описаны в псевдопакете builtin и обрабатываются компилятором. Из них два относятся к созданию значений: new(T) выделяет обнулённую память под T и возвращает *T, а make(T, ...) работает только для трёх встроенных ссылочных типов — slice, map, chan — и возвращает готовое к работе значение типа T, а не указатель. Никаких alloc, calloc, realloc, resize в Go нет: за размещение и переразмещение отвечают рантайм и append, за освобождение — сборщик мусора. Решение «стек или куча» принимает компилятор по результатам escape-анализа, а не программист.
Второй смысловой блок — «Go и ООП». Go не объектно-ориентирован в смысле Java/C++: нет классов, конструкторов, наследования, виртуальных таблиц у структур, super. Но есть три из четырёх привычных кирпичей — инкапсуляция (экспорт по регистру первой буквы, границы пакета), полиморфизм (интерфейсы, неявно удовлетворяемые) и абстракция. Вместо наследования — композиция и встраивание (embedding): встроенное поле продвигает свои методы наверх, но это не подтип, а делегирование, поэтому нет ковариантности и нет вызова «переопределённого» метода из базового кода. Каноничная формула на собеседовании: «интерфейсы описывают поведение и объявляются на стороне потребителя; структуры — данные и детали; связывает их композиция».
Третий блок — инструментарий. gofmt снимает споры о форматировании, go vet ловит подмножество реальных ошибок и запускается автоматически при go test, golangci-lint агрегирует десятки анализаторов (в первую очередь staticcheck, govet, errcheck, revive, ineffassign), dlv — единственный полноценный отладчик, pprof/runtime/trace/-race покрывают производительность и гонки, а go generate + go/ast/text/template — кодогенерацию. С Go 1.24 инструменты-зависимости описываются прямо в go.mod через директиву tool (go get -tool, go tool <name>), что заменило хак с tools.go и пустыми импортами.
Вопросы и ответы
Заголовок раздела «Вопросы и ответы»Какой у вас опыт коммерческой разработки на Go и над какими проектами работали?
Заголовок раздела «Какой у вас опыт коммерческой разработки на Go и над какими проектами работали?»Коротко. Вопрос про личный опыт: интервьюер хочет за 2–3 минуты понять масштаб систем, вашу роль и глубину участия. Формула ответа: сколько лет и в каком контексте → 1–2 проекта по схеме «домен → нагрузка → стек → что делал лично я → результат в цифрах» → чем готов углубиться.
Глубже. Что реально слушают: (1) масштаб — RPS, объём данных, число сервисов, размер команды; (2) вашу зону ответственности — «писал хендлеры» и «спроектировал схему обмена и выкатил её на 12 сервисов» звучат по-разному; (3) конкретику стека — Postgres/pgx, Kafka/NATS, gRPC/HTTP, k8s, observability; (4) умение назвать проблему и её решение (деградация под нагрузкой, утечка горутин, миграция без даунтайма). Каркас: «3 года в коммерческой разработке на Go. Последний проект — биллинг: ~2k RPS в пике, Postgres + pgx, 6 сервисов на gRPC, я отвечал за сервис списаний — переписал транзакционную часть на outbox, что убрало рассинхрон с платёжным шлюзом и снизило p99 с 800 до 120 мс». Типичные ошибки: перечисление технологий без ролей и результатов; уход в 15-минутный монолог; невозможность назвать ни одной цифры; ответ «делали микросервисы» без домена.
Чем make отличается от new ?
Заголовок раздела «Чем make отличается от new ?»Коротко. new(T) выделяет обнулённую память под тип T и возвращает *T; работает с любым типом. make(T, ...) применим только к slice, map и chan, выполняет их внутреннюю инициализацию и возвращает само значение типа T, а не указатель.
Глубже. Разница не в «стеке против кучи» (это решает escape-анализ), а в том, что для slice/map/chan недостаточно обнулённой памяти: слайсу нужен указатель на массив, длина и ёмкость; мапе — заголовок с таблицами и seed хеша; каналу — кольцевой буфер, очереди ожидающих и мьютекс. new(map[string]int) даст *map[string]int, указывающий на nil-мапу — запись в неё паникует. new в реальном коде используют редко: &T{} читается лучше и позволяет сразу задать поля; new уместен для указателя на скаляр (p := new(int)).
p := new(int) // *int, *p == 0s := new([]int) // *[]int, *s == nil, append(*s, 1) сработает, но так не пишутm := make(map[string]int) // map[string]int, готова к записиch := make(chan int, 8) // буферизованный каналsl := make([]int, 0, 100) // len 0, cap 100var bad map[string]intbad["k"] = 1 // panic: assignment to entry in nil mapКакой линтер для Go вам нравится больше всего и за что?
Заголовок раздела «Какой линтер для Go вам нравится больше всего и за что?»Коротко. На практике — golangci-lint как раннер, а внутри него ядро из staticcheck, govet, errcheck, ineffassign, revive. Ценность в том, что это один бинарь, который параллельно гоняет десятки анализаторов, кеширует результаты и умеет --new-from-rev для постепенного внедрения в старый код.
Глубже. Содержательно самый сильный отдельный анализатор — staticcheck (Dominik Honnef): он находит не стилистику, а реальные баги — недостижимый код, неверные форматные строки, бессмысленные сравнения, некорректное использование time.Tick, sync типов по значению, потерянные context. go vet идёт в комплекте с тулчейном и запускается на go test автоматически; из него особенно полезны copylocks, loopclosure, printf, httpresponse. Отдельно стоит errcheck (проигнорированные ошибки, в т.ч. defer f.Close() на записи) и bodyclose. Хороший ответ включает и меру: конфигурировать набор линтеров явно в .golangci.yml, включать в CI с --timeout, не тащить gochecknoglobals/funlen в легаси-репозиторий сразу, иначе шум убьёт доверие к линтеру.
Go is a relatively new language. How did you come to it?
Заголовок раздела «Go is a relatively new language. How did you come to it?»Коротко. Вопрос про мотивацию, не про факты. Отвечайте связкой «откуда пришёл → какая задача заставила попробовать → что оказалось решающим». Полезно упомянуть, что язык не такой уж новый: публичный релиз — ноябрь 2009, Go 1 — март 2012, то есть за плечами больше десяти лет стабильной обратной совместимости.
Глубже. Хорошие содержательные аргументы «почему Go», которые звучат честно: статическая типизация плюс скорость компиляции; один статически слинкованный бинарь и тривиальный деплой в контейнер; горутины и каналы как дешёвая модель конкурентности вместо колбеков или тредпулов; маленькая спецификация — новый человек в команде читает чужой код на второй день; сильный стандартный тулчейн из коробки (go test, pprof, race, gofmt). Ошибка — отвечать «модно/везде вакансии» или, наоборот, начинать с критики предыдущего языка.
Можно ли в Go функцию передать как параметр в другую функцию?
Заголовок раздела «Можно ли в Go функцию передать как параметр в другую функцию?»Коротко. Да. Функции в Go — значения первого класса: их можно передавать аргументами, возвращать, класть в поля структур, слайсы и мапы, замыкать над переменными.
Глубже. Тип функции задаётся сигнатурой: func(int) (string, error). Замыкание захватывает переменные по ссылке — с Go 1.22 переменная цикла for создаётся заново на каждой итерации, поэтому классическая ловушка «все горутины видят последнее значение i» в новых версиях исчезла (при go >= 1.22 в go.mod). Метод можно взять как значение (v.Method — method value, ресивер захвачен) или как выражение (T.Method — первым параметром идёт ресивер). Стандартная библиотека активно на этом построена: sort.Slice, http.HandlerFunc, slices.SortFunc, sync.OnceFunc, errors.Is с кастомным Is.
func apply(xs []int, f func(int) int) []int { out := make([]int, len(xs)) for i, v := range xs { out[i] = f(v) } return out}
func main() { k := 3 _ = apply([]int{1, 2, 3}, func(v int) int { return v * k }) // замыкание над k}Есть ли в Go исключения?
Заголовок раздела «Есть ли в Go исключения?»Коротко. Нет. Ошибки — обычные значения типа error, возвращаемые последним результатом; для действительно исключительных ситуаций есть panic/recover, но это не механизм управления потоком и не замена ошибкам.
Глубже. panic разворачивает стек, выполняя отложенные функции; recover() внутри defer останавливает разворачивание и возвращает значение паники. Отличия от исключений: нельзя перехватить по типу без ручной проверки, нет finally (есть defer), нет проверяемых исключений, panic не пересекает границу горутины — паника в горутине кладёт весь процесс, если не перехвачена в ней же. Уместные случаи panic: нарушение инварианта, невозможная ветка, ошибка инициализации в init/MustXxx. Уместные случаи recover: граница библиотеки или воркера, чтобы одна плохая задача не уронила сервис (так делает net/http — он перехватывает панику хендлера, кроме http.ErrAbortHandler). Для работы с ошибками-значениями в стандартной библиотеке есть обёртка fmt.Errorf("...: %w", err) и errors.Is/errors.As/errors.Join (последняя — с Go 1.20).
Что в Go нравится и что не нравится?
Заголовок раздела «Что в Go нравится и что не нравится?»Коротко. Вопрос на зрелость: нужно назвать конкретные сильные и слабые стороны с обоснованием, а не «нравится всё» и не «дженерики кривые». Плюсы: простота и читаемость, конкурентность, тулчейн, гарантия совместимости Go 1. Минусы: многословность обработки ошибок, nil-интерфейс с ненулевым типом, отсутствие sum-типов и иммутабельности.
Глубже. Конкретика, которая звучит профессионально. Нравится: единый форматтер и отсутствие споров о стиле; escape-анализ и дешёвые горутины; context как сквозная отмена; стандартная библиотека, на которой реально можно писать прод (net/http, encoding/json, database/sql, с 1.21 — log/slog, slices, maps); совместимость — код 2015 года собирается сегодня. Не нравится: if err != nil каждые пять строк и отсутствие принятого решения после отклонения try/проекта обработки ошибок; типизированный nil в интерфейсе (var p *T = nil; var i any = p; i != nil); отсутствие enum и алгебраических типов, из-за чего исчерпывающая проверка вариантов не проверяется компилятором; дженерики без методов с параметрами типа и без вариативности; отсутствие const для составных значений. Ошибка — критиковать то, чего не понимаете (например, «GC тормозит» без цифр).
Где в Go создаются объекты? Кто и как этим управляет?
Заголовок раздела «Где в Go создаются объекты? Кто и как этим управляет?»Коротко. Значения размещаются либо на стеке горутины, либо в куче; решение принимает компилятор на этапе escape-анализа, а не программист. Кучей управляет рантайм: аллокатор в стиле tcmalloc (mcache → mcentral → mheap) и конкурентный трёхцветный mark-and-sweep сборщик мусора.
Глубже. Если компилятор доказал, что значение не переживёт вызов и не «убежит» (не сохраняется в глобал, не возвращается указателем, не попадает в интерфейс, уходящий наружу, не захвачено горутиной), оно кладётся на стек — это бесплатно и не нагружает GC. Посмотреть решение можно через go build -gcflags='-m -m'. Стек горутины начинается с 8 КБ (contiguous stack) и растёт копированием с переустановкой указателей. Куча разбита на арены (64 МБ на 64-битных платформах), внутри — span-ы страниц, поделённые на объекты фиксированных размерных классов (около 67 классов до 32 КБ); каждый P имеет локальный кеш mcache, поэтому типичная аллокация — это bump-указатель без блокировок. Объекты больше 32 КБ выделяются напрямую из mheap целыми страницами. Освобождение — только сборщиком мусора; ручного free нет, финализаторы (runtime.SetFinalizer, с Go 1.24 — более безопасный runtime.AddCleanup) не гарантируют времени вызова и не годятся для управления ресурсами: для этого defer и io.Closer.
Для реализации микросервиса можно пользоваться только стандартной библиотекой Go (последней версии) и библиотекой pgx для доступа к БД.
Заголовок раздела «Для реализации микросервиса можно пользоваться только стандартной библиотекой Go (последней версии) и библиотекой pgx для доступа к БД.»Коротко. Это условие тестового задания, а не вопрос. Ограничение выполнимо без потерь: net/http + ServeMux с методами и wildcard-паттернами (Go 1.22), encoding/json, log/slog, context, errors, os/signal для graceful shutdown, pgxpool для БД, testing + httptest для тестов.
Глубже. Каркас, который ожидают увидеть: слои transport (http) → service (домен) → repository (pgx), интерфейсы объявлены на стороне потребителя (сервис описывает нужный ему UserRepo, реализация — в пакете postgres); конструкторы принимают зависимости явно, без глобалов и DI-контейнера; context.Context первым аргументом сквозь все слои и передаётся в pool.Query(ctx, ...). Маршрутизация в 1.22+ позволяет писать mux.HandleFunc("GET /users/{id}", h.get) и брать r.PathValue("id") — сторонний роутер не нужен. Сервер поднимается как &http.Server{Handler: mux, ReadHeaderTimeout: 5*time.Second}, останавливается по signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) через srv.Shutdown(ctx). Для pgx: один *pgxpool.Pool на процесс, настроенные MaxConns/MaxConnLifetime, транзакции через pgx.BeginFunc/tx.Rollback() в defer, массовые вставки через CopyFrom, батчи через pgx.Batch. Логи — slog.New(slog.NewJSONHandler(os.Stdout, nil)) с прокидыванием request-id через контекст.
mux := http.NewServeMux()mux.HandleFunc("GET /users/{id}", h.getUser)
srv := &http.Server{Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)defer stop()
go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error("listen", "err", err) }}()<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()_ = srv.Shutdown(shutdownCtx)Как Go решает проблему фрагментации?
Заголовок раздела «Как Go решает проблему фрагментации?»Коротко. Внешнюю фрагментацию кучи убирают размерные классы: объекты одинакового размерного класса живут в отдельных span-ах, поэтому освободившийся слот всегда подходит под новый объект того же класса. Внутренняя фрагментация ограничена сверху (округление до классов даёт не более ~12.5% перерасхода). Компактизации (перемещения объектов) в Go нет — GC неперемещающий.
Глубже. Механика: mheap управляет страницами по 8 КБ, page allocator выдаёт непрерывные диапазоны страниц под span; span целиком принадлежит одному размерному классу (67 классов до 32 КБ, отдельно — noscan/scan варианты). Аллокация — из mcache конкретного P без блокировок, при исчерпании — из mcentral (общий список span-ов класса), при исчерпании — из mheap. Пустые span-ы возвращаются в heap и могут быть переиспользованы под другой класс. Память отдаётся ОС фоновым scavenger-ом (madvise(MADV_DONTNEED/MADV_FREE)); скорость возврата регулируется, а с Go 1.19 есть GOMEMLIMIT — мягкий лимит, заставляющий GC работать чаще, вместо надежды на возврат страниц. Стеки горутин фрагментации почти не создают: они непрерывны и растут копированием. Что действительно может выглядеть как фрагментация — долгоживущая мапа или слайс, которые не отдают память после удаления элементов (delete не уменьшает мапу, s = s[:0] не уменьшает cap); лечится пересозданием контейнера. На 64-битных системах адресное пространство велико, поэтому проблема резервирования диапазонов практически не возникает.
Что делает встроенная функция make и чем она отличается от new ?
Заголовок раздела «Что делает встроенная функция make и чем она отличается от new ?»Коротко. См. выше про make и new. Кратко: make инициализирует внутреннюю структуру slice/map/chan и возвращает готовое значение этого типа; new даёт указатель на обнулённую память любого типа.
Глубже. Дополнение по сигнатурам, которое обычно спрашивают следом: make([]T, len) и make([]T, len, cap) — второй аргумент обязателен для слайса; make(map[K]V) и make(map[K]V, hint) — hint это подсказка о размере, не жёсткая ёмкость (и cap для мапы не определён); make(chan T) даёт небуферизованный канал, make(chan T, n) — буфер на n элементов. Оба builtin вычисляются во время выполнения; make с отрицательной длиной или len > cap для константных аргументов — ошибка компиляции, для переменных — паника в рантайме.
Какой встроенный тип в Go предназначен для хранения одного символа Unicode?
Заголовок раздела «Какой встроенный тип в Go предназначен для хранения одного символа Unicode?»Коротко. rune — псевдоним (alias) для int32, хранящий одну кодовую точку Unicode. byte — alias для uint8 и хранит именно байт, а не символ.
Глубже. Строка в Go — неизменяемая последовательность байт, обычно в UTF-8. Индексация s[i] даёт byte; for i, r := range s декодирует UTF-8 и даёт rune вместе с байтовым индексом; некорректная последовательность отдаёт utf8.RuneError (U+FFFD) и сдвигает индекс на 1. []rune(s) конвертирует строку в слайс кодовых точек с аллокацией, len(s) считает байты, utf8.RuneCountInString(s) — руны. Важно помнить, что «символ» в бытовом смысле (графемный кластер вроде «é» из e + U+0301 или эмодзи с модификаторами) может состоять из нескольких рун — для этого нужен golang.org/x/text.
s := "Привет"fmt.Println(len(s)) // 12 байтfmt.Println(utf8.RuneCountInString(s)) // 6 рунvar r rune = 'П'fmt.Printf("%c %d %U\n", r, r, r) // П 1055 U+041FС какой целью в Go можно давать label циклам и как это влияет на управление потоком?
Заголовок раздела «С какой целью в Go можно давать label циклам и как это влияет на управление потоком?»Коротко. Метка позволяет break/continue адресовать внешний цикл, а не ближайший. Это единственный штатный способ выйти сразу из вложенного цикла или из цикла, внутри которого стоит switch/select (иначе break съест switch).
Глубже. Метка — идентификатор с двоеточием перед оператором for, switch или select. break label завершает помеченный оператор, continue label переходит к следующей итерации помеченного цикла (только для for). Есть ещё goto label — прыжок внутри функции, запрещено перепрыгивать объявления переменных и входить в блок извне; на практике goto встречается разве что в сгенерированном коде и в рантайме. Метки живут в отдельном пространстве имён и обязаны использоваться, иначе — ошибка компиляции label defined and not used.
outer: for i := 0; i < len(grid); i++ { for j := 0; j < len(grid[i]); j++ { switch grid[i][j] { case 0: continue outer // к следующей строке, а не к следующему j case -1: break outer // без метки прервался бы только switch } } }Какие инструменты синхронизации в Go ты знаешь?
Заголовок раздела «Какие инструменты синхронизации в Go ты знаешь?»Коротко. Каналы и select (передача владения данными), пакет sync (Mutex, RWMutex, WaitGroup, Once, Cond, Pool, Map), пакет sync/atomic (типизированные atomic.Int64, atomic.Pointer[T] и др. с Go 1.19), context для отмены и дедлайнов, плюс golang.org/x/sync (errgroup, semaphore, singleflight).
Глубже. Как выбирать: канал — когда данные передаются между горутинами или нужно координировать этапы; мьютекс — когда несколько горутин читают/пишут общее состояние в одном месте. RWMutex выигрывает только при действительно преобладающих чтениях и небольшом числе ядер — на многоядерных машинах кеш-линия счётчика читателей становится узким местом, и обычный Mutex часто быстрее. sync.Once (и производные OnceFunc/OnceValue/OnceValues из Go 1.21) — ленивая инициализация. sync.Pool — переиспользование временных объектов между циклами GC, не кеш. sync.Map оправдан в двух сценариях из документации: ключ пишется один раз и много раз читается, либо разные горутины работают с непересекающимися ключами. sync.Cond нужен редко и почти всегда заменяется каналом. Все типы sync нельзя копировать после первого использования — go vet (анализатор copylocks) это ловит. Формальная база — модель памяти Go (https://go.dev/ref/mem): корректность даёт не «атомарность», а отношение happens-before.
Представьте, что нужно обработать текстовый файл с данными пользователей. При небольшом размере файла проблем нет. Но как вы будете читать, обрабатывать и формировать файл, если его размер достигает нескольких сотен мегабайт или даже гигабайт?
Заголовок раздела «Представьте, что нужно обработать текстовый файл с данными пользователей. При небольшом размере файла проблем нет. Но как вы будете читать, обрабатывать и формировать файл, если его размер достигает нескольких сотен мегабайт или даже гигабайт?»Коротко. Только потоково: os.Open + bufio.Scanner/bufio.Reader для чтения по строкам, обработка на лету, bufio.Writer + явный Flush для записи. Никаких os.ReadFile/io.ReadAll — память должна быть O(размер строки), а не O(размер файла).
Глубже. Практические детали, которые отличают хороший ответ. (1) bufio.Scanner по умолчанию ограничен токеном в 64 КБ и на длинной строке вернёт bufio.ErrTooLong — нужно sc.Buffer(make([]byte, 0, 1<<20), 8<<20) либо bufio.Reader.ReadString('\n'), у которого лимита нет. (2) После цикла обязательно проверять sc.Err(). (3) sc.Bytes() возвращает слайс на внутренний буфер, переиспользуемый на следующей итерации — если значение сохраняется, нужен slices.Clone/string(...). (4) Для CSV — csv.Reader с ReuseRecord = true, чтобы не аллоцировать слайс на строку. (5) Если обработка CPU-тяжёлая — конвейер: одна горутина читает и шлёт батчи строк в канал, GOMAXPROCS воркеров считают, одна пишет; порядок восстанавливается индексом батча, если он важен. Батчи по 1000+ строк, а не по одной строке, иначе накладные расходы на канал съедят выигрыш. (6) Запись: bufio.NewWriterSize(f, 1<<20), defer w.Flush() — и проверять ошибку Flush, а не только Close; для гарантий на диске — f.Sync(). (7) Для сжатых входов — gzip.NewReader поверх bufio.Reader. (8) Если данные едут в Postgres — pgx CopyFrom с CopyFromFunc, а не построчные INSERT. (9) Прогресс и отменяемость: проверять ctx.Err() раз в N строк. (10) Записывать во временный файл и os.Rename в конце — атомарная замена.
f, err := os.Open(path)if err != nil { return err}defer f.Close()
sc := bufio.NewScanner(f)sc.Buffer(make([]byte, 0, 64*1024), 8*1024*1024) // до 8 МБ на строку
out, err := os.CreateTemp(filepath.Dir(dst), ".tmp-*")if err != nil { return err}w := bufio.NewWriterSize(out, 1<<20)
for sc.Scan() { line := sc.Bytes() // валиден только до следующего Scan if _, err := w.Write(transform(line)); err != nil { return err }}if err := sc.Err(); err != nil { return err}if err := w.Flush(); err != nil { return err}if err := out.Close(); err != nil { return err}return os.Rename(out.Name(), dst)Писал ли что-то для кодогенерации?
Заголовок раздела «Писал ли что-то для кодогенерации?»Коротко. Вопрос про опыт. Ожидают, что вы назовёте go generate как механизм запуска, конкретные генераторы, которыми пользовались (stringer, mockgen, sqlc, protoc-gen-go, easyjson, ent), и, если писали свой, — на чём: text/template для простых случаев, go/ast + go/types + golang.org/x/tools/go/packages для разбора исходников.
Глубже. Каркас ответа про собственный генератор: зачем (устранить ручную рутину и рассинхрон — например, генерировать конвертеры DTO↔domain или обвязку метрик по интерфейсу), как читали вход (packages.Load с NeedSyntax|NeedTypes|NeedTypesInfo — надёжнее, чем парсить AST руками, потому что доступна информация о типах), как писали выход (text/template → format.Source → запись файла с суффиксом _gen.go и комментарием // Code generated by X. DO NOT EDIT. в первой строке — этот шаблон распознают линтеры и git diff), как встроили (//go:generate go run ./cmd/gen -type=User в файле-источнике, go generate ./... в Makefile/CI, проверка в CI, что после генерации git diff --exit-code пуст). С Go 1.24 сам генератор удобно фиксировать как зависимость через go get -tool и вызывать go tool gen, без tools.go. Полезно упомянуть альтернативы: build-теги для платформенного кода, дженерики — часть задач, ради которых раньше генерировали код, теперь решаются типами-параметрами, и это правильный первый выбор.
What’s the difference between LittleEndian and BigEndian, do you remember?
Заголовок раздела «What’s the difference between LittleEndian and BigEndian, do you remember?»Коротко. Это порядок байт при представлении многобайтового числа в памяти или в потоке. Little-endian кладёт младший байт первым (x86, ARM в обычном режиме), big-endian — старший первым (сетевой порядок байт, используемый в TCP/IP). Число 0x01020304 в little-endian: 04 03 02 01, в big-endian: 01 02 03 04.
Глубже. В Go это пакет encoding/binary с переменными binary.LittleEndian, binary.BigEndian (обе реализуют binary.ByteOrder/AppendByteOrder) и binary.NativeEndian, добавленной в Go 1.21. Прямые методы PutUint32/Uint32 быстрее и аллокационно чище, чем рефлексивные binary.Read/binary.Write, — последние стоит использовать только для сложных структур и в некритичном по производительности коде. Правило выбора: в сетевых и файловых форматах порядок обязан быть зафиксирован явно (обычно big-endian, «network byte order»), внутри процесса — NativeEndian. Порядок байт не влияет на строки/байтовые массивы, только на многобайтовые числовые типы; UTF-8 тоже от endianness не зависит (в отличие от UTF-16, где нужен BOM).
var buf [4]bytebinary.BigEndian.PutUint32(buf[:], 0x01020304)fmt.Printf("% x\n", buf) // 01 02 03 04binary.LittleEndian.PutUint32(buf[:], 0x01020304)fmt.Printf("% x\n", buf) // 04 03 02 01Does Go’s scheduler work at the level of kernel threads?
Заголовок раздела «Does Go’s scheduler work at the level of kernel threads?»Коротко. Нет. Планировщик Go — пользовательского уровня: он мультиплексирует горутины (G) на потоки ОС (M) через логические процессоры (P), которых GOMAXPROCS. Планированием самих потоков по ядрам занимается ядро ОС; Go-планировщик решает только, какая горутина выполняется на каком M.
Глубже. Модель M:N, известная как GMP. У каждого P есть локальная runqueue (256 слотов) и есть глобальная очередь; свободный P крадёт работу у других (work stealing) и раз в 61 такт заглядывает в глобальную очередь ради справедливости. Переключение горутины стоит порядка сотни наносекунд и не требует системного вызова, потому что это просто смена регистров и указателя стека в пользовательском пространстве. Точки переключения: операции с каналами, mutex, сетевые операции (через netpoller на epoll/kqueue/IOCP), вызовы рантайма, вызовы функций с проверкой роста стека, а с Go 1.14 — асинхронная вытесняемость через сигнал SIGURG, что убрало зависание на «горячих» циклах без вызовов. Блокирующий системный вызов (файловый ввод-вывод, cgo) отвязывает M от P: sysmon замечает долгий syscall и отдаёт P другому M, поэтому число потоков процесса может заметно превышать GOMAXPROCS (лимит — runtime/debug.SetMaxThreads, по умолчанию 10000). runtime.LockOSThread жёстко привязывает горутину к потоку — нужно для OpenGL и некоторых системных API. В Go 1.25 GOMAXPROCS по умолчанию стал учитывать cgroup-лимиты CPU в контейнерах; в 1.22–1.24 он равен числу видимых ядер, и в Kubernetes его обычно выставляли вручную или через automaxprocs.
Is Go an OOP language at all?
Заголовок раздела «Is Go an OOP language at all?»Коротко. Частично. В Go есть инкапсуляция, абстракция и полиморфизм через интерфейсы, но нет классов, конструкторов, наследования и виртуальных таблиц у пользовательских типов. Правильная формулировка: Go поддерживает объектно-ориентированный стиль, но не является «классическим» ООП-языком.
Глубже. Официальный FAQ отвечает «Yes and no»: методы можно объявить на любом именованном типе (не только на структуре — например, на type Celsius float64), интерфейсы удовлетворяются неявно (структурная типизация), но иерархии типов нет. Диспетчеризация динамическая только через интерфейс: значение интерфейса — пара (itab с типом и таблицей методов, указатель на данные); вызов метода на конкретном типе статичен и часто инлайнится. Отсюда практические следствия: нельзя «переопределить» метод так, чтобы встроенный тип начал вызывать вашу версию (нет виртуальности у структур), нельзя присвоить []Dog в []Animal (нет ковариантности), нельзя объявить абстрактный класс с частичной реализацией — вместо этого встраивают интерфейс или дефолтную структуру.
Как оцениваете свой уровень Go от джуна до сеньора?
Заголовок раздела «Как оцениваете свой уровень Go от джуна до сеньора?»Коротко. Вопрос-калибровка: важна не цифра, а обоснование через конкретные умения и честно названные пробелы. Формула: назвать уровень → 2–3 факта, подтверждающих его → одна-две области, где сознательно слабее, и что с этим делаете.
Глубже. Что обычно считают признаками уровня: junior — пишет корректный код по образцу, знает синтаксис, срезы, мапы, базовые горутины; middle — самостоятельно проектирует пакет и API, уверенно работает с context, ошибками, тестами, понимает race и умеет его чинить, читает pprof; senior — проектирует сервис целиком и границы между сервисами, понимает GC/планировщик достаточно, чтобы объяснить наблюдаемое поведение, принимает решения о зависимостях и совместимости, разбирает чужие инциденты, влияет на практики команды. Ошибки: заявить «сеньор» и поплыть на nil-интерфейсе или на устройстве слайса; занизить себя до «джуна» с пятью годами опыта (интервьюер калибрует сложность вопросов по вашему ответу); отвечать числом «8 из 10» без содержания.
Go - это ООП или нет?
Заголовок раздела «Go - это ООП или нет?»Коротко. См. выше «Is Go an OOP language at all?». Кратко: объектно-ориентированный стиль поддерживается (методы, интерфейсы, инкапсуляция), классического ООП с классами и наследованием нет.
Глубже. Отличие этой формулировки в том, что по-русски её обычно задают как вход к следующему вопросу — «а что тогда вместо наследования». Поэтому сразу продолжайте: композиция и встраивание вместо наследования, интерфейсы на стороне потребителя вместо иерархий, отсутствие конструкторов компенсируется функциями NewXxx и валидным нулевым значением (sync.Mutex, bytes.Buffer, strings.Builder работают «из коробки» без инициализации — это идиома «make the zero value useful»).
Что в Go используется вместо наследования?
Заголовок раздела «Что в Go используется вместо наследования?»Коротко. Композиция и встраивание (embedding) плюс интерфейсы. Встроенное поле продвигает свои методы и поля во внешний тип, так что внешний тип автоматически удовлетворяет тем же интерфейсам, но это делегирование, а не подтипизация.
Глубже. Правила, о которых спрашивают: методы продвигаются с учётом множества методов ресивера (встраивание T даёт наружу методы с ресивером T, а если внешнее значение адресуемо — и с *T); конфликт имён на одной глубине не ошибка, пока к имени не обращаются, но снимает продвижение; ближайшая глубина побеждает. Внутри метода встроенного типа s.Method() вызовет реализацию встроенного типа, а не «переопределённую» во внешнем — виртуальности нет, паттерн Template Method так не сделать (его делают, передав интерфейс явным полем). Встраивание интерфейса в структуру — приём для частичной реализации: struct{ io.ReadWriter } даёт заглушку, где нереализованные методы паникуют при вызове; так делают, например, при частичной реализации больших интерфейсов в тестах и в grpc с UnimplementedXxxServer.
type Logger struct{ prefix string }
func (l Logger) Log(msg string) { fmt.Println(l.prefix, msg) }
type Service struct { Logger // встраивание: Service получает метод Log db *sql.DB}
var s Services.Log("started") // на самом деле s.Logger.Log("started")Чем в Go выражаются абстракции и детали?
Заголовок раздела «Чем в Go выражаются абстракции и детали?»Коротко. Абстракции — интерфейсы (и границы пакетов с экспортируемыми именами), детали — конкретные структуры, неэкспортируемые поля и функции. Идиома: интерфейс объявляется там, где он используется (в потребителе), а не рядом с реализацией; возвращают конкретные типы, принимают интерфейсы.
Глубже. Практически это значит: доменный слой описывает узкий интерфейс type UserStore interface { ByID(ctx context.Context, id int64) (User, error) }, а пакет postgres просто имеет подходящие методы и ничего не знает об интерфейсе — нет импортной зависимости от абстракции к реализации, инверсия зависимостей получается бесплатно. Размер интерфейса — важный сигнал: «the bigger the interface, the weaker the abstraction»; io.Reader с одним методом переиспользуется везде, интерфейс на 15 методов не абстракция, а копия структуры. Инкапсуляция реализуется регистром первой буквы и границей пакета (а не типа): внутри пакета всё видно всем файлам. Дополнительные инструменты сокрытия деталей: неэкспортируемые типы с экспортируемыми конструкторами, internal/-директории (импорт разрешён только из поддерева родителя), функциональные опции для расширяемой конфигурации, интерфейс с неэкспортируемым методом как «закрытая» сумма типов.
About projects and standard architecture and Go questions;
Заголовок раздела «About projects and standard architecture and Go questions;»Коротко. Обрывок описания секции интервью, а не вопрос. Смысл: блок про ваши проекты, типовую архитектуру сервисов и общие вопросы по Go.
Глубже. Если такое встречается в описании этапа, готовьте: рассказ о проекте по схеме «домен → нагрузка → стек → моя роль → результат»; типовую слоистую структуру Go-сервиса (cmd/, internal/, слои transport/service/repository, internal/ для непубличного кода) с обоснованием, почему именно так, а не «потому что так в шаблоне»; и базовый набор вопросов по языку — слайсы, мапы, интерфейсы, горутины, context, ошибки, GC.
Standard Go and experience questions.
Заголовок раздела «Standard Go and experience questions.»Коротко. См. предыдущий пункт: это тоже пометка о формате секции — стандартные вопросы по Go плюс вопросы про опыт, а не самостоятельный вопрос.
Глубже. Отличие от предыдущей формулировки только в акценте: здесь «standard Go» — про язык и стандартную библиотеку (что чаще всего значит net/http, encoding/json, context, sync, errors, testing), без темы архитектуры.
Какой механизм есть в Go - наследование или композиция?
Заголовок раздела «Какой механизм есть в Go - наследование или композиция?»Коротко. Композиция. Наследования в языке нет вообще: нет ключевого слова extends, нет базовых классов, нет виртуальных методов у структур. Есть встраивание, которое выглядит похоже синтаксически, но семантически это композиция с автоматическим делегированием.
Глубже. См. выше «Что в Go используется вместо наследования». Ключевое отличие от наследования, которое стоит проговорить: Service со встроенным Logger не является Logger в смысле подтипа — var l Logger = s не скомпилируется, []Service не приводится к []Logger, и переопределение метода во внешнем типе не влияет на вызовы изнутри встроенного. Полиморфизм в Go даёт только интерфейс.
Для чего нужны линтеры?
Заголовок раздела «Для чего нужны линтеры?»Коротко. Чтобы автоматически ловить то, что компилятор пропускает: реальные баги (проигнорированные ошибки, неверные форматные строки, копирование мьютексов, потерянный context), потенциальные уязвимости и отклонения от единого стиля. Это дешёвый способ снять с код-ревью механическую часть и оставить людям обсуждение архитектуры.
Глубже. Разделяйте три класса инструментов: форматтеры (gofmt, gofumpt, goimports) — детерминированный стиль, спорить не о чем; корректность (go vet, staticcheck, errcheck, bodyclose, nilness, sqlclosecheck) — находят дефекты; политика/стиль (revive, depguard, gochecknoglobals, lll) — соглашения команды. Ценность растёт, когда линтер обязателен в CI и одинаков локально и в пайплайне (одна версия golangci-lint, конфиг в репозитории). Стоит упомянуть ограничения: линтер даёт ложные срабатывания, поэтому нужны //nolint:name // причина с обоснованием, а не глобальные отключения; и он не заменяет тесты, -race и ревью — статический анализ не видит гонок и логических ошибок домена. Из специфического: go vet уже включён в go test, а govulncheck (golang.org/x/vuln) проверяет известные CVE в зависимостях и, в отличие от обычных сканеров, сообщает только о реально достижимом уязвимом коде.
Инструменты для отладки?
Заголовок раздела «Инструменты для отладки?»Коротко. dlv (Delve) — интерактивный отладчик, включая dlv attach и dlv core; -race — детектор гонок; pprof (CPU, heap, goroutine, mutex, block) и runtime/trace — производительность и поведение планировщика; GODEBUG и runtime/debug — диагностика рантайма; плюс логи log/slog и тесты как первый инструмент воспроизведения.
Глубже. Практический набор по ситуациям. Зависание/утечка горутин: curl /debug/pprof/goroutine?debug=2 из net/http/pprof — полные стеки всех горутин; или послать SIGQUIT процессу (GOTRACEBACK=all) и получить дамп. Гонка: go test -race ./... и сборка стенда с -race (замедление в 2–20 раз и ~5–10x по памяти, в проде обычно не держат). Утечка памяти: heap-профиль с -base между двумя снимками, GODEBUG=gctrace=1 для картины по циклам GC. Проблемы с латентностью и планировщиком: runtime/trace и go tool trace, GODEBUG=schedtrace=1000. Аллокации в горячем пути: go test -bench . -benchmem и -memprofile, затем go tool pprof -alloc_objects. Проверка решений компилятора: go build -gcflags='-m -m' (escape-анализ) и -gcflags=-S (ассемблер). Отдельно — expvar и метрики из runtime/metrics для постоянного наблюдения. Delve полезно уметь запускать не только в IDE: dlv debug ./cmd/app -- -flag, dlv test ./pkg -- -test.run TestX, точки останова по условию break pkg.Func + cond, goroutines -t для списка горутин со стеками.
Что из нижеперечисленного верно об использовании ключевого слова new в Go?
Заголовок раздела «Что из нижеперечисленного верно об использовании ключевого слова new в Go?»Коротко. Верно: new — встроенная функция (не ключевое слово), принимает тип, выделяет обнулённую память под него и возвращает указатель *T. Неверные варианты обычно: «инициализирует значение», «работает с map/chan/slice, делая их готовыми к использованию», «выделяет память в куче», «требует освобождения».
Глубже. Разбор по пунктам, которые чаще всего предлагают как варианты. (1) new — это builtin, а не ключевое слово: его имя можно затенить локальной переменной (new := 5 компилируется, хотя так писать не надо). (2) Возвращает всегда указатель, даже для скаляров. (3) Память гарантированно обнулена (нулевое значение типа), а не «мусор», как в C. (4) Куча или стек — определяет escape-анализ; new сам по себе кучу не подразумевает. (5) Для slice/map/chan new даёт указатель на nil-значение, работать с ним нельзя — нужен make. (6) Освобождать вручную не нужно, этим занимается GC. (7) new(T) эквивалентен &T{} для композитных типов, и второй вариант в Go идиоматичнее.
calloc ;
Заголовок раздела «calloc ;»Коротко. Вариант ответа из теста «какая функция выделяет память в Go» — неверный. calloc — функция C (stdlib.h), в Go её нет. Появиться в Go-коде она может только через cgo как C.calloc.
Глубже. Смысл различия: calloc в C выделяет и обнуляет память, malloc — только выделяет. В Go обнуление гарантировано всегда, поэтому отдельная «calloc-подобная» функция не нужна: и new, и make, и композитные литералы дают нулевое значение. Через cgo C.calloc/C.free иногда всё же используют, когда буфер передаётся в C-библиотеку и должен пережить вызов, — такую память GC не видит и освобождать её нужно вручную.
realloc ;
Заголовок раздела «realloc ;»Коротко. Тоже вариант ответа и тоже неверный: realloc — из C. Аналог по смыслу в Go — встроенный append, который при нехватке cap сам выделяет новый массив, копирует данные и возвращает новый слайс.
Глубже. Отличие от C принципиальное: realloc может расширить блок на месте и вернуть тот же указатель, а append этого не гарантирует — поэтому результат append обязателен к присваиванию, а старый слайс продолжает указывать на прежний массив. Стратегия роста в Go менялась: до 1.18 — удвоение до 1024 элементов и +25% дальше; с Go 1.18 переход плавнее (порог ~256 элементов и постепенное снижение коэффициента), плюс округление итогового размера до размерного класса аллокатора, поэтому фактический cap может оказаться больше запрошенного. Уменьшить cap append-ом нельзя — только созданием нового слайса и copy (или slices.Clip, который обрезает cap до len без копирования).
Коротко. Правильный вариант: make — встроенная функция Go для создания и инициализации slice, map и chan. Возвращает значение соответствующего типа, а не указатель.
Глубже. См. подробности выше в вопросах про make и new. Единственное, что добавлю здесь: make — не универсальный «аллокатор». Для структур используют &T{}/new(T), для массивов — объявление var a [N]T (массив фиксированного размера, значение, а не ссылка), для строк — конкатенацию/strings.Builder, для буферов — bytes.Buffer с полезным нулевым значением.
alloc ;
Заголовок раздела «alloc ;»Коротко. Неверный вариант: функции alloc в Go нет ни как builtin, ни в стандартной библиотеке.
Глубже. Слово alloc встречается в Go в другом контексте — в диагностике: runtime.MemStats.Alloc/TotalAlloc, профиль -alloc_space/-alloc_objects в pprof, метрика /memory/classes/heap/objects:bytes в runtime/metrics, столбец allocs/op в выводе go test -benchmem. Если в тесте видите alloc как вариант «функции выделения памяти» — это дистрактор.
resize .
Заголовок раздела «resize .»Коротко. Неверный вариант: встроенной функции resize в Go нет.
Глубже. Смысловые аналоги: для слайса «увеличить» — append или slices.Grow(s, n) (гарантирует место ещё под n элементов без лишних перевыделений), «уменьшить длину» — s = s[:n], «уменьшить ёмкость» — slices.Clip(s) или копия в новый слайс. Для мапы «резайза» нет вовсе: удаление элементов не уменьшает занятую память, единственный способ — создать новую мапу; clear(m) (builtin с Go 1.21) удаляет все элементы, но память тоже не возвращает.
utils ;
Заголовок раздела «utils ;»Коротко. Вариант ответа на вопрос про пакеты — неверный: пакета utils в стандартной библиотеке Go нет.
Глубже. Заодно это анти-паттерн именования: utils, helpers, common, misc — пакеты без внятной ответственности, которые притягивают всё подряд и со временем создают циклические зависимости. Идиома Go — называть пакет по тому, что он предоставляет (http, bytes, slog), а функцию — коротко, чтобы вызов читался как фраза: bytes.Contains, а не byteutils.BytesContains. Если общий код действительно нужен, его кладут в пакет с осмысленным именем внутри internal/.
C.manager ;
Заголовок раздела «C.manager ;»Коротко. Тоже неверный вариант: такого пакета нет. Псевдопакет C в Go существует только в cgo-файлах и даёт доступ к C-типам и функциям, но никакого C.manager в нём нет — C.<имя> разрешается в реальный C-идентификатор из подключённых заголовков.
Глубже. Как работает cgo, если спросят: файл импортирует "C", а комментарий непосредственно перед импортом — это преамбула на C (#include, #cgo CFLAGS/LDFLAGS). Вызов C-функции переводит горутину в режим системного вызова, отвязывая M от P, поэтому cgo-вызовы дороги (сотни наносекунд плюс потенциальное создание потока) и не подлежат вытеснению планировщиком. Указатели на Go-память нельзя долго держать в C — действуют cgo pointer passing rules, проверяемые с GODEBUG=cgocheck=1/2.
reflect ;
Заголовок раздела «reflect ;»Коротко. Верный вариант: reflect — реальный пакет стандартной библиотеки, дающий интроспекцию типов и значений во время выполнения (reflect.TypeOf, reflect.ValueOf, чтение тегов структур, динамический вызов методов).
Глубже. На нём построены encoding/json, encoding/xml, database/sql (сканирование в any), fmt, валидаторы и ORM. Ограничения: изменять можно только адресуемые и экспортируемые поля (CanSet), иначе паника; ошибки типов ловятся в рантайме, а не при компиляции; накладные расходы велики — типичный рефлексивный маршалинг в разы медленнее сгенерированного кода, отсюда популярность easyjson/ffjson/sqlc. Практическое правило из стандартной библиотеки: «reflection is never clear» — если задачу можно решить интерфейсом, дженериком или кодогенерацией, рефлексию не берут. Полезные детали для собеседования: reflect.DeepEqual не годится для сравнения в проде (медленно, странно ведёт себя с nil vs пустым слайсом и с функциями) — в тестах лучше google/go-cmp; reflect.Type сравним и его можно класть в мапу как ключ (типовой кеш).
unsafe .
Заголовок раздела «unsafe .»Коротко. Верный вариант: unsafe — настоящий пакет стандартной библиотеки, позволяющий обходить систему типов: unsafe.Pointer, uintptr-арифметика, Sizeof/Alignof/Offsetof, а с Go 1.17/1.20 — unsafe.Slice, unsafe.SliceData, unsafe.String, unsafe.StringData.
Глубже. unsafe не подпадает под гарантию совместимости Go 1 в той же мере, что остальная библиотека, и нарушение правил конвертации unsafe.Pointer (описанных в документации пакета шестью разрешёнными паттернами) даёт неопределённое поведение — GC может освободить или переместить память, на которую вы держите uintptr. Легитимные применения: zero-copy преобразование []byte ↔ string в горячем пути (unsafe.String(unsafe.SliceData(b), len(b))), работа с C-структурами в cgo, низкоуровневые библиотеки сериализации, atomic над произвольной памятью. Проверять код с unsafe стоит go vet (unsafeptr) и -race; для памяти, приходящей от C, полезен -asan/-msan. Важный нюанс: строка, полученная через unsafe.String из изменяемого []byte, ломает инвариант неизменяемости строк — если байты потом поменяются, поведение мапы с такой строкой-ключом становится непредсказуемым.
Какие элементы в Go могут быть nil ?
Заголовок раздела «Какие элементы в Go могут быть nil ?»Коротко. Шесть категорий: указатели, слайсы, мапы, каналы, функции и интерфейсы. Плюс unsafe.Pointer. Всё остальное — числа, bool, строки, массивы, структуры — nil быть не может, у них другие нулевые значения.
Глубже. Что можно делать с nil-значениями каждого вида — частый уточняющий вопрос. nil-слайс: len, cap, range, append работают, индексация паникует; nil-слайс и пустой слайс различимы по s == nil, но не по len, и в JSON сериализуются по-разному (null против []). nil-мапа: чтение даёт нулевое значение, len, range, delete безопасны, запись паникует. nil-канал: чтение и запись блокируются навсегда (это используют, чтобы «выключить» ветку select), close(nil-канала) паникует. nil-функция: вызов паникует. nil-указатель: разыменование паникует, но вызвать метод с pointer-ресивером на нём можно, если метод не трогает поля (классический пример — методы на *Node в дереве). nil-интерфейс: интерфейс равен nil, только если и тип, и значение внутри нулевые; типизированный nil (var p *T = nil; var e error = p) даёт e != nil — самая известная ловушка языка, из-за которой не стоит объявлять переменную конкретного указательного типа для ошибки и возвращать её как error.
type MyErr struct{}func (*MyErr) Error() string { return "boom" }
func bad() error { var p *MyErr // nil return p // интерфейс НЕ nil: тип *MyErr, значение nil}
fmt.Println(bad() == nil) // falseЧто произойдет при попытке доступа к элементу карты в Go по ключу, которого нет в карте?
Заголовок раздела «Что произойдет при попытке доступа к элементу карты в Go по ключу, которого нет в карте?»Коротко. Паники не будет: вернётся нулевое значение типа значения мапы. Отличить «нет ключа» от «есть ключ с нулевым значением» позволяет форма с двумя результатами — v, ok := m[k].
Глубже. Это работает и для nil-мапы: чтение из неё тоже безопасно и даёт нулевое значение. Практические следствия: map[string]bool как множество читается напрямую (if set[k]), потому что нулевое значение false совпадает по смыслу с «нет элемента»; а map[string]int для счётчиков позволяет писать m[k]++ без предварительной инициализации. Обратная сторона — молчаливые баги, когда ноль в данных неотличим от отсутствия: тогда либо comma-ok, либо map[string]*T, где отсутствие даёт nil. Ещё нюансы: элемент мапы не адресуем (&m[k] — ошибка компиляции), и нельзя присвоить поле структуры-значения напрямую (m[k].Field = 1 не компилируется для map[K]StructType) — нужно читать, менять, класть обратно, или хранить указатели.
Go не поддерживает иммутабельность для срезов и карт;
Заголовок раздела «Go не поддерживает иммутабельность для срезов и карт;»Коротко. Утверждение верное. const в Go работает только для булевых, числовых, строковых и рунных констант; объявить неизменяемый слайс, мапу или структуру нельзя, и readonly-ссылок вроде const T& в C++ в языке нет.
Глубже. Как с этим живут на практике. (1) Передавать копию: для слайса — slices.Clone, для мапы — maps.Clone (оба из Go 1.21); копия поверхностная, вложенные ссылки остаются общими. (2) Прятать данные: хранить слайс/мапу в неэкспортируемом поле и отдавать наружу либо копию, либо итератор (iter.Seq из Go 1.23, maps.Keys/slices.Values возвращают именно итераторы). (3) Использовать массивы: [N]T — значимый тип, копируется при присваивании и передаче, и его можно объявить как значение структуры-константы по духу. (4) Строки действительно неизменяемы — если нужен неизменяемый набор байт, string подходит лучше []byte. (5) Документировать контракт: комментарий «возвращённый слайс менять нельзя» — распространённое, хоть и не гарантируемое компилятором решение (так делает, например, http.Request.Header в части общих значений). Стоит помнить и обратное: append к слайсу, отданному наружу, может незаметно записать в чужой массив, если cap > len, — от этого спасает slices.Clip или трёхиндексный срез s[a:b:b].
Как в Go можно объявить пустой срез целых чисел?
Заголовок раздела «Как в Go можно объявить пустой срез целых чисел?»Коротко. Три способа: var s []int (nil-слайс, идиоматично), s := []int{} (пустой не-nil слайс) и s := make([]int, 0) (то же, с явной аллокацией, можно задать ёмкость: make([]int, 0, 100)).
Глубже. Разница между nil и пустым: len и cap у обоих ноль, append работает одинаково, range не выполняет ни одной итерации, но s == nil различается, и encoding/json кодирует nil как null, а пустой — как []. Идиома из Go Code Review Comments: предпочитать var s []int, потому что nil-слайс не аллоцирует и ведёт себя как пустой; []int{} использовать только когда важна не-nil семантика (например, ответ API обязан содержать пустой массив). make([]int, 0, n) — правильный выбор, когда заранее известно число элементов: избавляет от перевыделений в цикле append. Ошибка новичка — make([]int, 10) вместо make([]int, 0, 10): первый вариант создаёт десять нулей, и последующий append добавит одиннадцатый элемент.
Как в Go проводится объявление переменной?
Заголовок раздела «Как в Go проводится объявление переменной?»Коротко. Двумя формами: полной var name Type = value (тип или значение можно опустить) и короткой name := value внутри функций. Плюс const для констант и объявление в скобках для групп. Неиспользуемая локальная переменная — ошибка компиляции.
Глубже. Детали, которые проверяют. var x int даёт нулевое значение (0, "", false, nil) — «zero value» гарантируется всегда. := работает только внутри функции и требует, чтобы хотя бы одна переменная слева была новой, иначе no new variables on left side of :=; в этой форме легко случайно затенить внешнюю переменную (особенно err внутри if/for — типичный источник багов, ловится shadow-анализатором go vet). Множественное присваивание a, b := 1, 2 и обмен a, b = b, a работают, потому что правая часть вычисляется целиком до присваивания. Групповое объявление var ( a int; b string ) и const с iota для перечислений. Область видимости — блочная, объявления на уровне пакета видны во всех файлах пакета и инициализируются в порядке зависимостей (не в порядке текста), затем выполняются функции init. Явное преобразование типов обязательно всегда: var f float64 = float64(i) — неявных числовых промоушенов нет; исключение — нетипизированные константы, которые подстраиваются под контекст.
var a int // 0var b, c = 1, "two" // типы выведеныd := 3.14 // float64const Pi = 3.14159 // нетипизированная константа
type Weekday intconst ( Sunday Weekday = iota // 0 Monday // 1)
_ = a; _ = b; _ = c; _ = d // blank identifier глушит "declared and not used"Какая функция в Go используется для создания файла?
Заголовок раздела «Какая функция в Go используется для создания файла?»Коротко. os.Create(name string) (*os.File, error) — создаёт файл (или обрезает существующий) с правами 0666 до применения umask. Более гибкий вариант — os.OpenFile(name, flag, perm); для временных файлов — os.CreateTemp; чтобы записать файл целиком одной операцией — os.WriteFile(name, data, perm).
Глубже. os.Create эквивалентен os.OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666) — обратите внимание на O_TRUNC: если файл существовал, его содержимое будет уничтожено. Если нужно «создать, только если не существует», берут флаг os.O_EXCL: os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) — это же атомарный способ реализовать lock-файл. Права perm — это os.FileMode, к которому ОС применяет umask процесса, поэтому фактические права обычно 0644 при запрошенных 0666. Директории создаются другими функциями: os.Mkdir/os.MkdirAll. Закрывать файл нужно всегда, но для записи defer f.Close() недостаточно — ошибку Close надо проверять, иначе потеря данных при сбросе буферов ОС останется незамеченной. С Go 1.24 появился os.Root — работа с файлами внутри каталога с защитой от выхода за его пределы через .. и симлинки: root, _ := os.OpenRoot(dir); f, _ := root.Create("a.txt").
Что такое new?
Заголовок раздела «Что такое new?»Коротко. new — встроенная функция Go: new(T) выделяет память под значение типа T, обнуляет её и возвращает указатель *T.
Глубже. См. развёрнутое сравнение выше. Здесь добавлю только формальность из спецификации: new — единственный builtin вместе с make, который принимает тип как первый аргумент; выражение new(T) эквивалентно &T{} для структурных, массивных, слайсовых и мапных типов-литералов, но new работает и там, где литерала нет (new(int), new([5]byte), new(chan int) — последний бесполезен, так как даёт указатель на nil-канал). В реальном коде new встречается редко; чаще его видно в стандартной библиотеке и в сгенерированном коде (protoc-gen-go использует new(T) для полей-указателей).
Какую функцию используют в Go для получения текущей рабочей директории?
Заголовок раздела «Какую функцию используют в Go для получения текущей рабочей директории?»Коротко. os.Getwd() (dir string, err error). Обратная операция — os.Chdir(dir).
Глубже. Нюансы: Getwd сначала пытается взять значение из переменной окружения PWD (если она указывает на тот же inode) и только затем делает системный вызов — поэтому в присутствии симлинков результат может быть «логическим» путём. Рабочая директория глобальна для всего процесса, а не для горутины, поэтому os.Chdir в конкурентном коде — источник трудноуловимых багов; вместо смены директории лучше строить абсолютные пути (filepath.Abs, filepath.Join) или использовать os.Root/os.OpenRoot (Go 1.24) и os.DirFS + io/fs для операций относительно каталога. В тестах на Go 1.24+ есть t.Chdir, который меняет директорию и корректно восстанавливает её после теста (при этом запрещает параллельные тесты). Не путайте с os.Executable() — это путь к бинарю, а не рабочая директория, и с os.UserHomeDir()/os.UserConfigDir().
os.Pwd() ;
Заголовок раздела «os.Pwd() ;»Коротко. Вариант ответа — неверный. Функции os.Pwd в Go нет; pwd — это утилита shell, а в Go рабочая директория читается через os.Getwd().
Глубже. Дистрактор основан на привычке из Unix. Родственная переменная окружения PWD действительно используется внутри реализации os.Getwd на Unix-системах, но публичного API с именем Pwd пакет не экспортирует.
os.Getwd() ;
Заголовок раздела «os.Getwd() ;»Коротко. Верный вариант: os.Getwd() возвращает абсолютный путь текущей рабочей директории и ошибку.
Глубже. См. подробности выше. Типовое использование: dir, err := os.Getwd(); if err != nil { ... } — ошибку возвращают, например, когда текущая директория удалена другим процессом.
os.Cwd() ;
Заголовок раздела «os.Cwd() ;»Коротко. Неверный вариант: такой функции в пакете os нет.
Глубже. Аббревиатура cwd (current working directory) широко используется в документации и в других языках (os.getcwd() в Python), но в Go имя другое — Getwd, унаследованное от POSIX-функции getwd/getcwd.
os.CurrentDir() ;
Заголовок раздела «os.CurrentDir() ;»Коротко. Неверный вариант: функции os.CurrentDir в Go нет.
Глубже. Ближайшее по смыслу в стандартной библиотеке: os.Getwd() — рабочая директория, filepath.Dir(p) — директория указанного пути, filepath.Abs(".") — абсолютный путь текущей директории (внутри он и вызывает Getwd).
os.GetDir() .
Заголовок раздела «os.GetDir() .»Коротко. Неверный вариант: os.GetDir не существует.
Глубже. Похожие реальные имена, чтобы не путаться: os.ReadDir(name) — список записей каталога ([]os.DirEntry, Go 1.16+, дешевле старого ioutil.ReadDir), (*os.File).ReadDir(n) — постраничное чтение, os.MkdirTemp — временный каталог, os.UserCacheDir/os.UserConfigDir/os.UserHomeDir — стандартные каталоги пользователя, os.TempDir() — каталог для временных файлов.
Какие функции пакета os в Go доступны для работы с пользовательскими правами на файлы?
Заголовок раздела «Какие функции пакета os в Go доступны для работы с пользовательскими правами на файлы?»Коротко. os.Chmod(name, mode) — права доступа; os.Chown(name, uid, gid) и os.Lchown — владелец и группа; методы (*os.File).Chmod и (*os.File).Chown — то же по дескриптору; прочитать права можно через os.Stat(name) → FileInfo.Mode(). Смежное: os.Chtimes для времён доступа/модификации.
Глубже. Права представлены типом os.FileMode (алиас fs.FileMode): младшие 9 бит — классические rwx для owner/group/other, старшие биты — флаги типа (ModeDir, ModeSymlink, ModeSetuid, ModeSticky). Получить только биты прав: info.Mode().Perm(). Платформенные особенности, которые полезно назвать: на Windows os.Chmod умеет менять только бит «только для чтения» (0200), а os.Chown всегда возвращает ошибку; os.Lchown меняет владельца самого симлинка, а не цели; при создании файла запрошенные права уменьшаются umask процесса, изменить который из Go напрямую нельзя (нужен syscall.Umask на Unix). Проверять доступ «можно ли писать» по битам режима ненадёжно (есть ACL, capabilities, root) — правильнее просто попробовать открыть файл и обработать ошибку через errors.Is(err, fs.ErrPermission).
os.GetPerms ;
Заголовок раздела «os.GetPerms ;»Коротко. Неверный вариант: такой функции нет.
Глубже. Чтобы получить права, используют os.Stat/os.Lstat, а затем FileInfo.Mode().Perm(). Для записи каталога быстрее os.ReadDir + DirEntry.Info(), потому что DirEntry даёт тип файла без дополнительного stat.
info, err := os.Stat("file.txt")if err != nil { return err}fmt.Printf("%v %04o\n", info.Mode(), info.Mode().Perm()) // -rw-r--r-- 0644os.ChangePermissions ;
Заголовок раздела «os.ChangePermissions ;»Коротко. Неверный вариант: функции os.ChangePermissions в Go нет. Права меняет os.Chmod.
Глубже. Имена в пакете os намеренно повторяют POSIX-вызовы (chmod, chown, stat, link, symlink, rename, unlink → Remove), а не «человеческие» английские фразы. Это хорошая эвристика при угадывании: если сомневаетесь, вспомните имя системного вызова.
os.SetUser ;
Заголовок раздела «os.SetUser ;»Коротко. Неверный вариант: os.SetUser не существует. Владельца меняет os.Chown(name, uid, gid).
Глубже. Информацию о пользователях читают не из os, а из пакета os/user: user.Current(), user.Lookup(name), user.LookupId(uid) — они дают *user.User с Uid, Gid, Username, HomeDir. Сменить пользователя у процесса стандартная библиотека не даёт (нет обёртки над setuid — она не потокобезопасна в модели Go); при запуске дочернего процесса это делается через exec.Cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: ..., Gid: ...}}.
os.SetPerms .
Заголовок раздела «os.SetPerms .»Коротко. Неверный вариант: os.SetPerms в Go нет. Правильный ответ на исходный вопрос — os.Chmod (и os.Chown для владельца).
Глубже. Единственная функция в os с префиксом Set для файлов — это методы (*os.File).SetDeadline/SetReadDeadline/SetWriteDeadline (Go 1.10+, работают для файлов, поддерживающих неблокирующий режим, — пайпы, терминалы, сокеты). Ещё os.Setenv — про переменные окружения, не про права.
Какую функцию в Go используют для создания символической ссылки на файл или директорию?
Заголовок раздела «Какую функцию в Go используют для создания символической ссылки на файл или директорию?»Коротко. os.Symlink(oldname, newname string) error, где oldname — цель ссылки, newname — путь создаваемой ссылки. Прочитать цель — os.Readlink, жёсткая ссылка — os.Link.
Глубже. Порядок аргументов совпадает с системным вызовом symlink(2) и часто путается: первым идёт то, «на что» указываем. Цель может не существовать — получится «висячая» ссылка, os.Symlink ошибки не даст. Отличать ссылку от обычного файла нужно через os.Lstat (не разыменовывает) и info.Mode()&fs.ModeSymlink != 0; os.Stat пойдёт по ссылке. Развернуть цепочку ссылок в реальный путь — filepath.EvalSymlinks. На Windows создание симлинков требует привилегии SeCreateSymbolicLinkPrivilege или режима разработчика, поэтому кроссплатформенный код должен корректно обрабатывать ошибку. Симлинки — источник уязвимостей (symlink attack) при работе с чужими каталогами: с Go 1.24 для этого есть os.Root, чей API отказывается следовать за ссылками наружу корня. При обходе дерева filepath.WalkDir по симлинкам не идёт — это защищает от циклов.
os.Symbolic() ;
Заголовок раздела «os.Symbolic() ;»Коротко. Неверный вариант: такой функции нет.
Глубже. Дистрактор от английского «symbolic link». Настоящее имя короче и повторяет системный вызов — os.Symlink.
os.LinkRef() ;
Заголовок раздела «os.LinkRef() ;»Коротко. Неверный вариант: os.LinkRef в Go отсутствует.
Глубже. Реальная соседняя функция — os.Link(oldname, newname), которая создаёт жёсткую ссылку: второй элемент каталога, указывающий на тот же inode. Отличия от симлинка: работает только в пределах одной файловой системы, нельзя ссылаться на каталог, файл физически удаляется только когда счётчик ссылок дойдёт до нуля, и «висячих» жёстких ссылок не бывает.
os.Reference() ;
Заголовок раздела «os.Reference() ;»Коротко. Неверный вариант: функции os.Reference нет.
Глубже. В пакете os вообще нет функций с «ссылочной» терминологией из объектных моделей — API держится POSIX-именований. Если в тесте предлагают набор из Symbolic, LinkRef, Reference, ReferenceFile, Symlink — верным будет только последний.
os.ReferenceFile() ;
Заголовок раздела «os.ReferenceFile() ;»Коротко. Неверный вариант: такой функции в Go нет.
Глубже. Похоже по написанию на реальную os.NewFile(fd uintptr, name string) *os.File — она оборачивает существующий файловый дескриптор в *os.File (пригодится для работы с fd, полученными от systemd socket activation или от родительского процесса через exec.Cmd.ExtraFiles).
os.Symlink() .
Заголовок раздела «os.Symlink() .»Коротко. Верный вариант: os.Symlink(oldname, newname) — создание символической ссылки.
Глубже. См. подробности выше в основном вопросе.
if err := os.Symlink("/var/releases/v42", "/var/app/current"); err != nil { return fmt.Errorf("symlink: %w", err)}target, err := os.Readlink("/var/app/current") // "/var/releases/v42"os.Methods ;
Заголовок раздела «os.Methods ;»Коротко. Вариант ответа на вопрос «что есть в пакете os» — неверный: типа или переменной os.Methods не существует.
Глубже. Пакет os экспортирует типы File, FileInfo (алиас fs.FileInfo), FileMode, DirEntry, Process, ProcessState, ProcAttr, LinkError, PathError (алиас fs.PathError), SyscallError, Root (Go 1.24), и переменные os.Stdin, os.Stdout, os.Stderr, os.Args, а также ошибки-сентинелы os.ErrNotExist, os.ErrPermission, os.ErrExist и т. д. (алиасы к io/fs).
os.Process ;
Заголовок раздела «os.Process ;»Коротко. Верный вариант: os.Process — реальный тип, представляющий запущенный процесс ОС.
Глубже. Связанное API: os.Getpid()/os.Getppid(), os.StartProcess (низкоуровневый запуск; в прикладном коде используют os/exec), методы (*os.Process).Signal, Kill, Wait, Release, а с Go 1.23 — os.Process.Handle-ориентированные os.PidfdSendSignal-подобные улучшения на Linux (рантайм использует pidfd, чтобы избежать гонок с переиспользованием PID). Результат Wait — *os.ProcessState с ExitCode(), Success(), SystemTime(). Для прикладных задач почти всегда правильнее exec.CommandContext(ctx, "prog", args...), потому что он даёт пайпы, отмену по контексту и *exec.ExitError с кодом возврата.
os.Routines ;
Заголовок раздела «os.Routines ;»Коротко. Неверный вариант: os.Routines не существует. Горутины к пакету os отношения не имеют — они часть рантайма языка.
Глубже. Информацию о горутинах дают runtime.NumGoroutine(), профиль runtime/pprof.Lookup("goroutine"), runtime.Stack(buf, true) и runtime/debug.PrintStack(). Число потоков ОС ограничивается debug.SetMaxThreads, а число активных P — runtime.GOMAXPROCS(n).
os.File ;
Заголовок раздела «os.File ;»Коротко. Верный вариант: os.File — тип, представляющий открытый файл (или любой файловый дескриптор), с методами Read, Write, Seek, Close, Stat, Sync, Truncate, Fd, Name.
Глубже. *os.File реализует io.Reader, io.Writer, io.Seeker, io.Closer, io.ReaderAt, io.WriterAt, поэтому подходит везде, где ожидаются эти интерфейсы (io.Copy, bufio, json.NewDecoder). Нулевое значение os.File бесполезно — работают только объекты из Open/Create/OpenFile/NewFile/Pipe. os.Stdin/Stdout/Stderr — это тоже *os.File. Важные детали: File.Fd() переводит дескриптор в блокирующий режим и выводит его из-под netpoller — после этого дедлайны не работают; File.Sync() вызывает fsync и нужен для durability; операции с *os.File потокобезопасны в смысле отсутствия гонки данных в самой структуре, но смещение файла общее, поэтому конкурентные Read/Write лучше делать через ReadAt/WriteAt.
os.Variables .
Заголовок раздела «os.Variables .»Коротко. Неверный вариант: os.Variables в Go нет.
Глубже. С переменными окружения работают функции os.Getenv, os.LookupEnv (возвращает value, ok — отличает пустое значение от отсутствия), os.Setenv, os.Unsetenv, os.Clearenv, os.Environ() (весь срез KEY=VALUE), os.ExpandEnv. В тестах с Go 1.17 удобен t.Setenv, который автоматически восстанавливает значение и запрещает t.Parallel().
os.sys .
Заголовок раздела «os.sys .»Коротко. Неверный вариант: os.sys не существует, к тому же имя с маленькой буквы не может быть экспортировано из пакета.
Глубже. Похожие по смыслу настоящие вещи: пакет syscall (заморожен, новые вызовы туда не добавляют), golang.org/x/sys/unix и golang.org/x/sys/windows (актуальный низкоуровневый доступ к системным вызовам), runtime.GOOS/runtime.GOARCH для определения платформы, (*os.ProcessState).Sys() и FileInfo.Sys() — методы, возвращающие платформозависимую структуру (*syscall.Stat_t на Unix), из которой достают inode, uid/gid, время создания.
Если у тебя нет опыта работы с Go - не беда, можешь воспринимать задание как некоторый псевдокод на Си-подобном языке. Будем говорить про то как в коде выделены абстракции и распределены зоны ответственности. Про то как можно улучшить код с точки зрения применения лучших практик проектирования и про то как сделать его надежным и расширяемым.
Заголовок раздела «Если у тебя нет опыта работы с Go - не беда, можешь воспринимать задание как некоторый псевдокод на Си-подобном языке. Будем говорить про то как в коде выделены абстракции и распределены зоны ответственности. Про то как можно улучшить код с точки зрения применения лучших практик проектирования и про то как сделать его надежным и расширяемым.»Коротко. Это формулировка задания на разбор кода (code review), а не вопрос со «знанием факта». Ожидают, что вы будете говорить не о синтаксисе, а о границах ответственности, зависимостях, обработке ошибок и расширяемости.
Глубже. Каркас разбора, который хорошо работает на таком формате. (1) Сначала прочитать и вслух сформулировать, что код делает и какие у него входы/выходы — это показывает, что вы не начинаете «чинить» непонятое. (2) Ответственность: у каждой функции/типа одна причина для изменения; если функция и парсит, и валидирует, и ходит в БД, и логирует — разделить. (3) Зависимости: направление импортов, зависимость от абстракций (интерфейс на стороне потребителя), никаких глобальных синглтонов и init-магии, зависимости передаются в конструктор. (4) Ошибки: не глотать, оборачивать с контекстом (%w), различать доменные ошибки и инфраструктурные, не логировать и возвращать одновременно, паниковать только на нарушении инвариантов. (5) Ресурсы и конкурентность: defer Close, context первым аргументом и его реальная передача вниз, таймауты на любой внешний вызов, отсутствие утечек горутин (у каждой горутины должен быть определён момент завершения). (6) Тестируемость как критерий качества дизайна: если для теста нужен реальный Postgres и сеть — границы проведены неудачно. (7) Расширяемость без переписывания: функциональные опции для конфигурации, маленькие интерфейсы, отсутствие «switch по типу» там, где нужен полиморфизм. (8) Что бы вы сделали в первую очередь и что оставили бы как есть — важно показать приоритизацию, а не выкатить список из 30 придирок. Типичная ошибка кандидата — уйти в стиль (имена, форматирование, «здесь можно короче»), не сказав ни слова про границы и ошибки.
Как обеспечить низкую связанность модулей при сохранении высокой связности внутри модуля?
Заголовок раздела «Как обеспечить низкую связанность модулей при сохранении высокой связности внутри модуля?»Коротко. Связность — держать вместе то, что меняется вместе (пакет по домену, а не по техническому слою). Низкая связанность — общаться через узкие интерфейсы, объявленные потребителем, передавать зависимости явно, ограничивать видимость через неэкспортируемые имена и internal/, и не пропускать типы одного модуля сквозь API другого.
Глубже. Конкретные приёмы в Go. (1) Организация: пакет order, billing, user (по домену) вместо models, services, repositories (по слою) — последнее гарантирует, что любое изменение фичи трогает три пакета сразу. (2) Инверсия зависимостей без фреймворка: интерфейс определяется там, где вызывается, реализация о нём не знает — благодаря неявному удовлетворению интерфейсов пакет-реализация не импортирует пакет-абстракцию. (3) Узость интерфейсов: один-три метода; чем шире интерфейс, тем сильнее связанность. (4) Не экспортировать больше, чем нужно; складывать внутренности в internal/, где компилятор физически запретит импорт извне поддерева. (5) Не протекать деталями: не возвращать наружу *pgx.Rows, *sql.Tx, protobuf-структуры или JSON-теги доменных типов — конвертировать на границе, даже ценой дублирования структур. (6) Асинхронная развязка: события/очередь вместо прямого вызова, когда получателей может стать несколько. (7) Контроль на уровне инструментов: go list -deps, визуализация графа импортов, линтер depguard, тесты архитектурных правил; циклический импорт Go запрещает сам, и это полезное давление на дизайн. (8) Критерий проверки: если для изменения одной фичи приходится править файлы в пяти пакетах — связность низкая; если изменение внутренней детали пакета ломает компиляцию у трёх потребителей — связанность высокая. Обе метрики важнее любых догм про «правильную структуру каталогов».
Что делает функция init() в пакете (Golang) и что не рекомендуется делать внутри нее.
Заголовок раздела «Что делает функция init() в пакете (Golang) и что не рекомендуется делать внутри нее.»Коротко. init() — специальная функция без аргументов и без возвращаемых значений, которую рантайм вызывает автоматически при инициализации пакета: после того как проинициализированы все импортируемые пакеты и все package-level переменные этого пакета, но до main(). Её нельзя вызвать или передать по имени. Внутри не стоит делать ничего, что может упасть, зависнуть или требовать конфигурации: сетевые/файловые операции, подключение к БД, чтение флагов, тяжёлые вычисления, panic и os.Exit.
Глубже. Порядок такой: сначала рекурсивно инициализируются импортируемые пакеты (каждый ровно один раз, независимо от числа импортов), затем в текущем пакете вычисляются package-level переменные в порядке зависимостей друг от друга (а не в порядке объявления), затем выполняются все init() этого пакета. Спецификация не гарантирует порядок между файлами, но go передаёт компилятору файлы отсортированными по имени, поэтому на практике порядок — алфавитный по имени файла. Полагаться на это в коде — плохая идея: это не часть контракта, и при сборке другим инструментом порядок может измениться.
Что конкретно плохо и почему:
- Побочные эффекты, невидимые в вызывающем коде.
init()выполняется от одного факта импорта. Тесты, которые импортируют пакет ради одной функции, внезапно открывают соединение или пишут в глобальные переменные. - Ошибки некуда вернуть. Единственный способ сообщить о проблеме —
panic, то есть падение процесса ещё доmain(), часто без нормального логирования и без шанса на graceful-обработку. - Чтение конфигурации и флагов.
flag.Parse()вызывается вmain(), значит вinit()флаги ещё не разобраны. Аналогично с переменными окружения, которые вы, возможно, хотите валидировать и красиво отчитаться об ошибке. - Долгая работа. Всё, что делает
init(), задерживает старт процесса и попадает в время readiness-пробы. Тяжёлое (прогрев кеша, компиляция шаблонов) лучше делать лениво черезsync.Onceили явно в конструкторе. - Скрытые зависимости между init’ами. Код, работающий из-за алфавитного порядка файлов, ломается при переименовании файла.
Легитимные применения: регистрация в глобальных реестрах (sql.Register в драйверах БД, image.RegisterFormat, регистрация типов protobuf/gob), инициализация констант-подобных значений (regexp.MustCompile, скомпилированные шаблоны), проверка инвариантов сгенерированного кода. Именно ради init() существует blank-импорт _ "github.com/jackc/pgx/v5/stdlib".
Что такое func init и как использовать?
Заголовок раздела «Что такое func init и как использовать?»Коротко. См. выше: это функция инициализации пакета, вызываемая рантаймом один раз до main(). Использовать её стоит только для регистрации и подготовки неизменяемого глобального состояния; всё, что может вернуть ошибку или зависит от конфигурации, выносить в явный конструктор.
Глубже. Канонический пример — саморегистрация реализации в реестре, чтобы вызывающий код не знал про конкретный тип:
package pgstore
import ( "database/sql"
"github.com/jackc/pgx/v5/stdlib")
func init() { sql.Register("pgx-traced", tracedDriver{base: &stdlib.Driver{}})}Разница между init() и инициализатором переменной: переменную можно инициализировать выражением или вызовом функции (var re = regexp.MustCompile(...)), и это предпочтительнее, если инициализация — одно выражение. init() нужен, когда действий несколько, когда есть условная логика, или когда нужно только побочное действие без переменной. Ещё одна деталь: init() может быть объявлен в main-пакете и выполнится до main(); в тестовом бинарнике init() пакета и тестовых файлов выполняются до TestMain/тестов.
Можно ли в одном пакете сделать несколько func init?
Заголовок раздела «Можно ли в одном пакете сделать несколько func init?»Коротко. Да. init() может быть сколько угодно — и несколько в одном файле, и в разных файлах пакета. Они выполнятся все, последовательно, в одной горутине; внутри файла — в порядке объявления, между файлами — в порядке, в котором файлы переданы компилятору (на практике алфавитном).
Глубже. init — единственная функция, для которой разрешено дублирование имени в пакете, потому что она не входит в область видимости пакета: её нельзя вызвать, взять её адрес или сослаться на неё. Компилятор собирает все init в сгенерированную функцию инициализации пакета. Практический вывод: несколько init — нормально для крупных пакетов с несколькими реестрами, но код не должен зависеть от их взаимного порядка. Если зависимость всё же нужна, лучше сделать один init, который в нужном порядке вызывает обычные функции.
Что такое iota?
Заголовок раздела «Что такое iota?»Коротко. iota — предопределённый идентификатор, доступный только внутри const-блока: счётчик, который равен 0 в первой строке блока (первом ConstSpec) и увеличивается на единицу с каждой следующей строкой. Используется для нумерованных констант и битовых масок.
Глубже. Ключевые нюансы, которые проверяют на собеседовании:
type State int
const ( StateUnknown State = iota // 0 StateRunning // 1 — выражение повторяется неявно StateStopped // 2)
const ( _ = iota // пропускаем 0 KB = 1 << (10 * iota) // 1 << 10 MB // 1 << 20 GB // 1 << 30)
const ( A, B = iota, iota + 1 // A=0, B=1 — iota одинакова в пределах строки C, D // C=1, D=2)iotaувеличивается на каждую строкуconst-блока (включая строки с_и строки, где выражение опущено), а не на каждую константу: две константы в одной строке видят одинаковое значение.- Счётчик сбрасывается в 0 в каждом новом
const (...). Разбив enum на два блока, вы начнёте нумерацию заново — классический источник багов. - Нулевое значение типа совпадает с первой константой. Поэтому первой обычно делают
Unknown/Invalid, чтобы «забыли проставить» не превращалось в валидное состояние. - Настоящих enum’ов в Go нет:
State(42)скомпилируется. Проверку исчерпанностиswitchдаёт линтерexhaustive, а строковое представление удобно генерироватьstringer(//go:generate stringer -type=State). iotaработает только с константными выражениями: строку"a"+iotaсобрать не выйдет, но1 << iota, арифметика и типизация — да.
Что такое graceful shutdown? Как его реализовать для HTTP-сервера?
Заголовок раздела «Что такое graceful shutdown? Как его реализовать для HTTP-сервера?»Коротко. Graceful shutdown — остановка сервиса без обрыва уже принятых запросов: перестаём принимать новые соединения, даём текущим запросам доработать в пределах таймаута, закрываем внешние ресурсы (БД, брокеры) и только потом выходим. Для net/http это Server.Shutdown(ctx) по сигналу SIGTERM/SIGINT.
Глубже. Shutdown закрывает listener’ы, закрывает idle keep-alive соединения и ждёт завершения активных запросов, пока не истечёт контекст; он возвращает ctx.Err(), если не успел, — тогда добивают Close(). ListenAndServe при этом немедленно возвращает http.ErrServerClosed, и это не ошибка.
package main
import ( "context" "errors" "log" "net/http" "os/signal" "syscall" "time")
func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop()
mux := http.NewServeMux() mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
srv := &http.Server{ Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second, }
errCh := make(chan error, 1) go func() { errCh <- srv.ListenAndServe() }()
select { case err := <-errCh: if !errors.Is(err, http.ErrServerClosed) { log.Fatalf("listen: %v", err) } case <-ctx.Done(): log.Println("shutdown signal received") }
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { log.Printf("graceful shutdown failed: %v", err) _ = srv.Close() } // здесь: закрыть пул БД, продюсеры Kafka, дождаться фоновых воркеров}Что важно добавить словами, потому что это и отличает senior-ответ:
- Порядок в Kubernetes. До
SIGTERMпод уже исключён из Endpoints, но обновление iptables/ingress асинхронно, поэтому классика — сначала перевести readiness-пробу в fail и подождать 5–10 секунд (preStop sleep), и только потом начинатьShutdown. Иначе часть трафика уедет в закрывающийся под. terminationGracePeriodSecondsдолжен быть больше вашего таймаута shutdown, иначе прилетитSIGKILLпосреди процесса.Shutdownне трогает hijacked-соединения (WebSocket, gRPC-стримы) и long polling — их нужно закрывать самому, например прокинув в хендлеры контекст черезServer.BaseContextи отменив его.- Фоновая работа: горутины, запущенные из хендлеров,
Shutdownне ждёт. Их считают черезsync.WaitGroup/errgroupи ждут после. - Двойной сигнал: второй
SIGTERMобычно должен приводить к немедленному выходу (signal.NotifyContextпослеstop()возвращает поведение по умолчанию).
Расскажи про reflection.
Заголовок раздела «Расскажи про reflection.»Коротко. Рефлексия — возможность во время выполнения узнать тип и структуру значения и работать с ним, не зная типа на этапе компиляции: пакет reflect, ключевые сущности reflect.Type и reflect.Value. На ней построены encoding/json, fmt, database/sql, валидаторы, ORM и DI-контейнеры.
Глубже. Механика опирается на устройство интерфейса: любое значение, положенное в any, — это пара (указатель на дескриптор типа runtime._type, указатель на данные). reflect.TypeOf(x) возвращает первый компонент, reflect.ValueOf(x) — обёртку над обоими. Отсюда «три закона рефлексии» Роба Пайка: из интерфейса можно получить reflect.Value; из reflect.Value можно вернуться в интерфейс (.Interface()); менять значение можно, только если оно адресуемо и экспортируемо — то есть если вы передали указатель и спустились через Elem(), а CanSet() вернул true.
func setField(dst any, name string, val any) error { v := reflect.ValueOf(dst) if v.Kind() != reflect.Pointer || v.IsNil() { return errors.New("need non-nil pointer") } f := v.Elem().FieldByName(name) if !f.IsValid() || !f.CanSet() { return fmt.Errorf("field %q is not settable", name) } rv := reflect.ValueOf(val) if !rv.Type().AssignableTo(f.Type()) { return fmt.Errorf("cannot assign %s to %s", rv.Type(), f.Type()) } f.Set(rv) return nil}Полезно знать: reflect.Kind (низкоуровневая категория: Struct, Slice, Pointer, …) отличается от reflect.Type (именованный тип); теги структур читаются через f.Tag.Get("json") и являются обычными строками, поэтому опечатка в теге молча ничего не сломает (ловится линтером structtag/go vet); reflect.DeepEqual удобен в тестах, но медленный и сравнивает неэкспортируемые поля (в тестах чаще берут google/go-cmp); с Go 1.22 есть reflect.TypeFor[T](), позволяющий получить тип без создания нулевого значения; с приходом дженериков (1.18) часть задач, где раньше была рефлексия, решается статически.
С какими профилями нагрузки Go лучше всего стравляется?
Заголовок раздела «С какими профилями нагрузки Go лучше всего стравляется?»Коротко. Лучше всего — сетевые сервисы с высокой конкурентностью и умеренной вычислительной нагрузкой: API-гейтвеи, микросервисы, прокси, брокерные консьюмеры, стриминг — то есть IO-bound с десятками тысяч одновременных соединений и требованием к предсказуемому p99 в единицы-десятки миллисекунд. Хуже всего — hard real-time, ультранизкие задержки в микросекундах и плотная численная арифметика/SIMD.
Глубже. Почему хорошо: goroutine + netpoller дают модель «поток на запрос» без цены потока ОС; блокирующий по виду код исполняется поверх epoll/kqueue; конкурентный GC с sub-миллисекундными stop-the-world паузами не портит хвосты; статический бинарник и малое потребление памяти на соединение хорошо ложатся на контейнеры и автоскейлинг.
Где Go проседает:
- Чистый CPU-bound счёт. Компилятор Go ориентирован на скорость сборки, а не на предельную оптимизацию: слабее инлайнинг, нет автовекторизации и intrinsics для SIMD, нет LTO. Типичный проигрыш C++/Rust — от 1.2× до нескольких раз на числодробилках. PGO (с 1.21) отыгрывает единицы процентов.
- Большие heap’ы, плотно набитые указателями. Стоимость маркировки пропорциональна количеству указателей; кеш из десятков миллионов мелких объектов даёт заметный CPU-оверхед GC. Лечение:
GOGC/GOMEMLIMIT(1.19), укрупнение объектов, структуры без указателей, off-heap/mmap,sync.Pool. - Жёсткий реалтайм и хвосты в микросекундах. Паузы GC малы, но не нулевые, а планировщик кооперативно-вытесняющий (асинхронное вытеснение сигналами с 1.14, шаг ~10 мс). Для HFT-подобных задач это неприемлемо.
- FFI-тяжёлые задачи. Каждый вызов через cgo стоит сотни наносекунд и ломает часть допущений планировщика.
Что можешь сказать про новые фитчи в версии 1.24?
Заголовок раздела «Что можешь сказать про новые фитчи в версии 1.24?»Коротко. Главное в Go 1.24: мапы переписаны на Swiss Tables (плюс общее ускорение рантайма на пару процентов), полноценные дженерик-алиасы типов, управление инструментами прямо в go.mod (go get -tool + go tool), testing.B.Loop для честных бенчмарков, os.Root для операций, ограниченных каталогом, слабые указатели (weak) и runtime.AddCleanup вместо SetFinalizer, тег omitzero в encoding/json, а также большой блок криптографии, включая режим FIPS 140-3 и новые пакеты (crypto/mlkem, crypto/hkdf, crypto/pbkdf2, crypto/sha3).
Глубже. По пунктам, чуть подробнее:
- Swiss Tables. Встроенная
mapтеперь — открытая адресация с группами по 8 слотов и control word; выигрыш заметнее всего на промахах и на крупных мапах. Реализация вinternal/runtime/maps. - Generic type aliases. Теперь можно
type Set[T comparable] = map[T]struct{}— алиас с параметрами типа, чего не хватало с 1.18. - Инструменты в
go.mod. Директиваtoolиgo get -tool golang.org/x/tools/cmd/stringerфиксируют версии утилит в модуле, аgo tool stringerих запускает. Это официальная замена трюку сtools.goи blank-импортами. testing.B.Loop.for b.Loop() { ... }вместоfor range b.N: компилятор не может выбросить тело как мёртвый код, а подготовка/финализация не попадают в измерение — надёжнее, чемruntime.KeepAliveи ручнойb.ResetTimer.os.Root.os.OpenRoot(dir)даёт хендл, все операции через который не могут выйти за пределы каталога (защита от path traversal и подмены симлинками).weakиruntime.AddCleanup. Слабые указатели для кешей/канонизации и корректная заменаSetFinalizer:AddCleanupне воскрешает объект, допускает несколько cleanup’ов и не течёт на циклах.json:",omitzero". Пропуск поля при нулевом значении типа, в отличие отomitempty, который не умеет отличать «пустой»time.Timeили нулевую структуру.- Итераторы в строках/байтах.
strings.Lines,strings.SplitSeq,strings.FieldsSeq(и аналоги вbytes) — ленивый обход без аллокации промежуточных срезов, продолжение линии range-over-func из 1.23. testing/synctest— экспериментальный (подGOEXPERIMENT=synctest) пакет для детерминированного тестирования конкурентного кода с фейковым временем.- Криптография. Режим FIPS 140-3 (
GODEBUG=fips140=on), постквантовыйcrypto/mlkem, вынесенные изx/cryptohkdf/pbkdf2/sha3.
Осторожная оговорка на собеседовании: «версии выходят каждые полгода, точный список фич сверяю с release notes» — лучше, чем уверенно назвать фичу не из той версии.
Какая разница между ключевыми словами make и new ?
Заголовок раздела «Какая разница между ключевыми словами make и new ?»Коротко. Формально это не ключевые слова, а встроенные функции. new(T) выделяет память под T, обнуляет её и возвращает *T. make(T, ...) работает только для слайса, мапы и канала: он не просто выделяет память, а инициализирует внутреннюю структуру этих типов и возвращает готовое значение типа T, не указатель.
Глубже. Различие вытекает из того, что слайс, мапа и канал — это дескрипторы, ссылающиеся на скрытые структуры рантайма. new(map[string]int) вернёт указатель на переменную-мапу, равную nil: писать в неё нельзя, будет panic: assignment to entry in nil map. make(map[string]int) создаёт runtime-структуру хеш-таблицы. Аналогично new([]int) даёт *[]int на нулевой слайс (len=0, cap=0, data=nil), а make([]int, 0, 16) выделяет массив под 16 элементов.
p := new(int) // *int, *p == 0s := make([]int, 3, 10) // []int, len 3, cap 10m := make(map[string]int, 100) // с подсказкой ёмкостиch := make(chan int, 8) // буферизованный канал
var nilMap map[string]int_ = nilMap["k"] // ок, вернёт 0// nilMap["k"] = 1 // паникаНа практике new в идиоматичном коде встречается редко: вместо new(T) пишут &T{}, что нагляднее и позволяет сразу заполнить поля. new остаётся удобным для примитивов, когда нужен указатель на значение по умолчанию (new(bool), new(atomic.Int64) — хотя и там чаще берут адрес переменной). Ещё нюанс: и new, и make не обязательно кладут объект в кучу — решает escape-анализ; p := new(int), не покидающий функцию, останется на стеке.
Reflection в Golang. Что такое, минусы/плюсы;
Заголовок раздела «Reflection в Golang. Что такое, минусы/плюсы;»Коротко. См. выше про рефлексию. Плюс один и большой: универсальный код для заранее неизвестных типов — сериализация, маппинг в БД, валидация по тегам, DI, тестовые сравнения. Минусы: потеря проверок на этапе компиляции (ошибки становятся паниками в рантайме), заметное замедление и лишние аллокации, нечитаемый код, сломанный рефакторинг и статический анализ, невозможность выкинуть неиспользуемый код при линковке.
Глубже. По каждому минусу конкретика:
- Скорость.
reflect.Value.Interface(),FieldByName(линейный поиск по имени с аллокацией),Callсо срезом[]reflect.Value— всё это на порядок дороже прямого вызова. Разница междуencoding/jsonи сгенерированным маршалером (easyjson,go-json,sonic) обычно 2–5× по времени и кратно по аллокациям. - Безопасность типов.
v.Field(3).SetInt(x)на структуре с другим набором полей — паника в рантайме, а не ошибка сборки. - Инструменты. Переименование поля не находит его использование в теге или в строке
FieldByName("Amount");go vetи IDE здесь бессильны. - Размер бинарника и
//go:linkname-эффекты. Компоновщик не может выкинуть методы, до которых «может дотянуться» рефлексия. - Конкурентность.
reflect.Valueне даёт никаких дополнительных гарантий; всё, что было небезопасно, остаётся небезопасным.
Практическое правило: рефлексию нормально использовать на границе системы (парсинг конфигов, сериализация, миграции) и один раз при старте; в горячем пути — нет. Часто помогает кеш: один раз через рефлексию построить план обработки типа (map[reflect.Type]plan) и дальше работать по нему. Дженерики закрывают часть кейсов статически, но не заменяют рефлексию там, где тип по-настоящему неизвестен до рантайма.
Почему в Go нет цикла while?
Заголовок раздела «Почему в Go нет цикла while?»Коротко. Потому что for покрывает все его формы. Авторы языка сознательно минимизировали грамматику: один цикл вместо трёх (while, do-while, for), в языке всего 25 ключевых слов. for cond { } — это и есть while, for { } — бесконечный цикл.
Глубже. Все формы:
for i := 0; i < n; i++ { } // классический трёхчастныйfor cond { } // аналог whilefor { } // бесконечный, аналог while(true)for i, v := range s { } // по слайсу/мапе/каналу/строкеfor range 10 { } // по целому числу, Go 1.22+for v := range seq { } // по функции-итератору iter.Seq, Go 1.23+Аналога do { } while (cond) тоже нет — пишут for { ...; if !cond { break } }. Это часть общей линии: нет тернарного оператора, нет перегрузки операторов, нет исключений, нет наследования. Мотивация — читаемость и однозначность: любой Go-код выглядит одинаково у всех, а компилятор и инструменты (gofmt, go vet, автоформат в IDE) работают с простой грамматикой. На собеседовании ответ «for покрывает все случаи, а лишние конструкции не добавляют выразительности, зато добавляют вариативность стиля» полностью достаточен.
Полезно упомянуть ещё две вещи: с Go 1.22 переменная цикла создаётся заново на каждой итерации (исчез классический баг с захватом i в замыкании и в горутине), а с Go 1.23 range умеет работать с функциями-итераторами (iter.Seq, iter.Seq2), что и дало maps.Keys, slices.Values, strings.Lines.
Какие в Go есть утилиты для оптимизации?
Заголовок раздела «Какие в Go есть утилиты для оптимизации?»Коротко. Базовый набор: бенчмарки go test -bench . -benchmem + benchstat для сравнения, профилировщик go tool pprof (CPU, heap, block, mutex, goroutine), трассировка go tool trace, escape-анализ go build -gcflags='-m', детектор гонок -race, PGO (default.pgo), ручки рантайма GOGC/GOMEMLIMIT/GODEBUG=gctrace=1 и метрики runtime/metrics.
Глубже. По слоям.
Измерение.
go test -run='^$' -bench=. -benchmem -count=10 ./... > new.txtbenchstat old.txt new.txt-count обязателен: одиночный прогон ничего не доказывает, benchstat покажет разброс и p-value. С Go 1.24 тело бенчмарка правильнее писать как for b.Loop() { ... }.
Профилирование.
go test -bench=. -cpuprofile=cpu.out -memprofile=mem.outgo tool pprof -http=:8080 cpu.outВ проде — net/http/pprof на служебном порту: /debug/pprof/profile?seconds=30 (CPU), /heap, /goroutine?debug=2 (дедлоки и утечки горутин), /block и /mutex (нужно предварительно включить runtime.SetBlockProfileRate и SetMutexProfileFraction). Для heap важно различать inuse_space (что занято сейчас — утечки) и alloc_space (что аллоцировалось всего — давление на GC). Сравнение двух профилей: pprof -base old.out new.out.
Трассировка. runtime/trace + go tool trace trace.out показывает то, чего не видно в pprof: задержки планировщика, время в syscall, паузы и фазы GC, блокировки на каналах, распределение по P. С Go 1.22 трассировка низкооверхедная и потоковая.
Компилятор.
go build -gcflags='-m -m'— escape-анализ: почему значение уехало в кучу.go build -gcflags='-m'заодно печатает решения об инлайнинге;-gcflags='-l'его отключает для экспериментов.go tool objdump,-gcflags=-S, и godbolt — когда нужно смотреть ассемблер.- PGO: положить
default.pgoрядом сmain— компилятор использует профиль для инлайнинга и девиртуализации; типичный выигрыш 2–7%. go tool nm -size,-ldflags='-s -w',go build -trimpath— про размер бинарника.
Рантайм и продовые ручки. GOGC (частота GC), GOMEMLIMIT (мягкий лимит памяти, с 1.19 — правильная замена «баллласту»), GOMAXPROCS (в контейнерах раньше приходилось ставить руками через automaxprocs; в Go 1.25 рантайм научился учитывать cgroup-лимит), GODEBUG=gctrace=1,schedtrace=1000, пакет runtime/metrics для экспорта в Prometheus.
Корректность, без которой оптимизация бессмысленна. -race, go vet, staticcheck, -d=checkptr при работе с unsafe, фаззинг go test -fuzz.
Порядок работы, который стоит проговорить: сначала бенчмарк/профиль на реальной нагрузке, потом самый жирный узел, потом повторное измерение. Типичные находки в Go-сервисах — лишние аллокации ([]byte↔string, fmt.Sprintf в горячем пути, отсутствие strings.Builder, отсутствие преаллокации make([]T, 0, n)), рефлексивный JSON, чрезмерная синхронизация и слишком мелкие задачи в отдельных горутинах.
стандартные вопросы про Go и БД.
Заголовок раздела «стандартные вопросы про Go и БД.»Коротко. «Стандартный» блок — это database/sql как пул соединений (SetMaxOpenConns/SetMaxIdleConns/SetConnMaxLifetime), обязательное закрытие *sql.Rows и проверка rows.Err(), QueryRowContext и sql.ErrNoRows, NULL через sql.Null*/указатели, транзакции с defer tx.Rollback(), контекст с таймаутом на каждый запрос, prepared statements и их конфликт с pgbouncer в режиме transaction pooling, и защита от SQL-инъекций через плейсхолдеры.
Глубже. Что именно обычно спрашивают и как отвечать.
*sql.DB— это не соединение, а пул. Его создают один раз на приложение и передают дальше;Openне устанавливает соединение (проверка —PingContext). Настройки:SetMaxOpenConns(обязательно, иначе пул неограничен и вы упрётесь вmax_connectionsPostgres),SetMaxIdleConns(обычно равен MaxOpen, чтобы не переустанавливать соединения),SetConnMaxLifetime(важно за балансировщиком, чтобы соединения обновлялись),SetConnMaxIdleTime.- Утечка соединений. Самая частая ошибка —
rowsбезdefer rows.Close(): соединение не возвращается в пул, и через N запросов сервис встаёт. Соединение освобождается автоматически, только еслиrows.Next()дошёл до конца. И обязательноif err := rows.Err(); err != nilпосле цикла — иначе оборванный поток строк выглядит как пустой результат. - Контексты. Все вызовы —
...Context-версии, с таймаутом. Отмена контекста прерывает запрос (в pgx/lib/pq шлётся cancel), но соединение при этом может быть закрыто — это нормальная плата. - NULL. Скан в
stringприNULLвернёт ошибку. Варианты:sql.NullString,*string,COALESCEв запросе. - Транзакции.
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}),defer tx.Rollback()(после успешногоCommitвернётsql.ErrTxDone, это ожидаемо и игнорируется). Внутри транзакции все запросы обязаны идти черезtx, иначе они уедут на другое соединение. Важно: транзакция держит соединение пула всё время жизни, поэтому «поход в другой сервис внутри транзакции» — антипаттерн. - Драйверы.
lib/pqсчитается legacy, актуальный выбор для Postgres —pgx(нативный интерфейс быстрее, но можно использовать и черезdatabase/sqlсовместимыйstdlib). Надстройки:sqlx(StructScan),sqlc/jet(кодогенерация из SQL — типобезопасно и быстро), GORM (удобно, но рефлексия, скрытые запросы и N+1). - Prepared statements и pgbouncer. В transaction/statement pooling подготовленные выражения ломаются; лечится
?default_query_exec_mode=simple_protocol/prefer_simple_protocolу pgx или отключением statement cache. - Инъекции. Только плейсхолдеры (
$1в Postgres,?в MySQL). Динамические имена таблиц/колонок — через белый список, а не конкатенацию. - Прочее, что любят спрашивать: N+1 и его устранение через
IN (...)/JOIN/батчинг, идемпотентность и ретраи приdeadlock detected/serialization failure, уровни изоляции, миграции (goose,golang-migrate,atlas), пакетная вставка (COPYчерез pgx, multi-rowINSERT), таймауты на стороне БД (statement_timeout), outbox-паттерн для согласованности с брокером, и тестирование черезtestcontainers-goвместо моков.
За что отвечает рефлексия и как она работает? В чем недостаток?
Заголовок раздела «За что отвечает рефлексия и как она работает? В чем недостаток?»Коротко. См. выше два ответа про рефлексию: она отвечает за интроспекцию и манипуляцию значениями по их рантайм-типу, а работает за счёт того, что интерфейсное значение хранит указатель на дескриптор типа и указатель на данные — reflect просто даёт к ним типизированный доступ. Главный недостаток — перенос ошибок с этапа компиляции в рантайм плюс серьёзная цена по скорости и аллокациям.
Глубже. Небольшое дополнение к сказанному ранее — про «как именно работает». Дескриптор типа (runtime._type, он же abi.Type) генерирует компилятор для каждого типа программы: там размер, выравнивание, kind, хеш, битовая карта указателей для GC, функции сравнения и хеширования, а для структур — таблица полей с именами, смещениями и тегами. Поэтому reflect не «декомпилирует» программу, а читает готовые метаданные; смещения полей известны, и Field(i) — это арифметика с указателем, а вот FieldByName — поиск по именам. Метод Value.Interface() собирает интерфейс обратно и при этом обычно аллоцирует. Ограничение на неэкспортируемые поля (CanSet() == false, CanInterface() == false) — искусственное, вводится самим reflect для сохранения инкапсуляции, а не физическое.
Отсюда практический вывод: если поверх рефлексии строится библиотека, разумно один раз при первом обращении к типу построить «план» (список смещений, функции-конверторы) и закешировать его в sync.Map по reflect.Type — так делают быстрые сериализаторы и sqlx.
Что в Go устраивает, а что не нравится?
Заголовок раздела «Что в Go устраивает, а что не нравится?»Коротко. Устраивает: обещание совместимости Go 1, gofmt и отсутствие споров о стиле, скорость сборки, богатая стандартная библиотека, один статический бинарник, встроенные тесты/бенчмарки/профилировщик/race-детектор, горутины и каналы. Не нравится: многословная обработка ошибок, ловушка типизированного nil в интерфейсе, отсутствие sum-типов и настоящих enum’ов, ограниченность дженериков, невозможность выразить «поле обязательно» из-за нулевых значений, struct tags как строки.
Глубже. Это вопрос на зрелость, а не на факты: интервьюер проверяет, есть ли у вас личное отношение и понимаете ли вы компромиссы. Хороший ответ — назвать 2–3 плюса и 2–3 минуса и для каждого минуса сказать, чем он оплачен и как вы с ним живёте.
Пример развёрнутой формулировки: «Меня раздражает if err != nil каждые три строки, но я понимаю, что это плата за явность потока управления: в Go по коду видно все точки отказа, чего не даёт код на исключениях. Живу с этим через fmt.Errorf("...: %w", err), errors.Is/As и helper-функции, а предложение try из 2019-го, честно говоря, сделало бы код менее читаемым. Что действительно мешает — отсутствие sum-типов: полиморфный результат приходится выражать интерфейсом с неэкспортируемым методом и switch без проверки исчерпанности, и линтер exhaustive тут только костыль».
Ещё пункты, которые уместно упомянуть: отсутствие иммутабельности (const только для скалярных констант, нет const-корректности); дженерики без методов с параметрами типа и с местами слабым выводом; context.Context первым аргументом почти везде — эффективно, но зашумляет сигнатуры; time, os/exec и encoding/json с их историческими шероховатостями (в 1.24 частично лечится omitzero, а в 1.25 появился экспериментальный encoding/json/v2). Чего говорить не стоит: «в Go нет ООП и это плохо», «нет исключений — язык примитивный» — такие ответы читаются как непонимание дизайна.
Читал ли про новые фитчи Go 1.24 и если дa, то что понравилось?
Заголовок раздела «Читал ли про новые фитчи Go 1.24 и если дa, то что понравилось?»Коротко. См. разбор 1.24 выше. Из практического мне ценнее всего три вещи: tool-директива в go.mod (версии stringer, mockgen, golangci-lint наконец живут в модуле, а не в README), testing.B.Loop (бенчмарки перестали врать из-за выброшенного мёртвого кода) и os.Root (закрывает целый класс уязвимостей с path traversal). Плюс бесплатное ускорение мап на Swiss Tables.
Глубже. Такой вопрос — проверка «следите ли вы за языком». Правильная структура ответа: назвать пару фич, объяснить какую боль они снимают в вашем коде, и честно сказать, что не пробовали. Например: «weak + runtime.AddCleanup — то, чего не хватало для кешей-канонизаторов: раньше приходилось выбирать между утечкой и ручной инвалидацией, а SetFinalizer был минным полем (не вызывался на циклах, воскрешал объект, не дружил с встроенными объектами). AddCleanup этих проблем не имеет. Дженерик-алиасы закрыли раздражающую дыру с 1.18: type Result[T any] = ... раньше не компилировался. testing/synctest пока экспериментальный и я его в проде не использую, но за ним слежу — детерминированные тесты конкурентного кода с виртуальным временем это то, чего в Go реально не хватает».
Расскажи про функции new и make
Заголовок раздела «Расскажи про функции new и make»Коротко. См. выше про разницу make/new: new(T) — выделить и обнулить память под любой тип, вернуть *T; make — только для слайса, мапы и канала, возвращает готовое к использованию значение самого типа, а не указатель.
Глубже. Дополнение про сигнатуры и рантайм. make — единственная встроенная функция с типом в качестве первого аргумента и переменным числом остальных: make([]T, len), make([]T, len, cap), make(map[K]V), make(map[K]V, hint), make(chan T), make(chan T, buf). Компилятор превращает их в вызовы runtime.makeslice, runtime.makemap/makemap_small, runtime.makechan (либо в стековую аллокацию, если escape-анализ разрешил). Паника при make([]int, -1) или при len > cap — рантайм-ошибка, если размер вычисляемый, и ошибка компиляции, если константный.
Практические советы, которые ждут в ответе: всегда указывайте cap, если знаете размер (make([]T, 0, n) экономит цепочку реаллокаций), а для мапы — hint (экономит перестроения). new для слайса/мапы/канала синтаксически валиден, но почти всегда является ошибкой в намерениях автора.
Что может предложить Go в плане многопоточной обработки?
Заголовок раздела «Что может предложить Go в плане многопоточной обработки?»Коротко. Горутины (дешёвые пользовательские потоки, планируемые рантаймом на пул потоков ОС), каналы и select как основное средство коммуникации, пакет sync (Mutex, RWMutex, WaitGroup, Once, Pool, Map), sync/atomic с типизированными атомиками (atomic.Int64, atomic.Pointer[T]), context для отмены и дедлайнов, golang.org/x/sync (errgroup, semaphore, singleflight) и встроенный детектор гонок.
Глубже. Ключевая идея: «Do not communicate by sharing memory; instead, share memory by communicating» — CSP-подход, где данные передаются через каналы, а владение данными в каждый момент времени принадлежит одной горутине. Но это не догма: счётчики и кеши в Go спокойно защищают мьютексом, и sync.Mutex быстрее канала на порядок.
Стандартные паттерны, которые полезно перечислить: worker pool (N горутин читают из одного канала задач), fan-in/fan-out, pipeline со стадиями, ограничение конкурентности через семафор или буферизованный канал, errgroup.WithContext для «запустить группу и отменить всё при первой ошибке», singleflight для схлопывания одинаковых запросов, graceful-остановка через context.Context + WaitGroup.
Модель памяти формализована в «The Go Memory Model»: гарантии happens-before дают старт горутины, отправка/приём по каналу, Mutex.Unlock/Lock, Once.Do, атомики (с Go 1.19 модель явно описывает атомики как sequentially consistent). Всё, что вне этих гарантий, — гонка, а гонка в Go — UB, а не «просто неточное значение». Отсюда обязательный go test -race в CI и go build -race в стейджинге.
Чего Go не даёт: приоритетов горутин, привязки к ядру (thread affinity, кроме runtime.LockOSThread), гарантий на порядок пробуждения (кроме FIFO-очередей у каналов и мьютекса в starvation mode), настоящей параллельной обработки без учёта GOMAXPROCS, а также структурированной конкурентности из коробки — её приходится собирать самому из errgroup и контекста.
Зачем сделали отдельный шедулер в Go если уже есть менеджер в ОС?
Заголовок раздела «Зачем сделали отдельный шедулер в Go если уже есть менеджер в ОС?»Коротко. Чтобы переключение между единицами конкурентности стоило десятки наносекунд вместо микросекунд и чтобы одна единица стоила килобайты вместо мегабайтов. Планировщик ОС оперирует потоками с фиксированным большим стеком и требует перехода в ядро на каждое переключение; Go-планировщик мультиплексирует миллионы горутин на небольшой пул потоков ОС (модель M:N) полностью в пространстве пользователя.
Глубже. Модель GMP: G — горутина (структура с состоянием и стеком), M — поток ОС, P — логический процессор (контекст выполнения, их ровно GOMAXPROCS, каждый со своей локальной run-queue на 256 элементов плюс глобальная очередь). Чтобы выполнять Go-код, M должен владеть P. Пустой P крадёт работу у соседей (work stealing).
Что это даёт по сравнению с «просто потоками ОС»:
- Цена переключения. Смена горутины — сохранение нескольких регистров и переход в другой стек, десятки наносекунд, без syscall, без сброса TLB и без обхода планировщика ядра. Переключение контекста потоков ОС — порядка микросекунды.
- Цена создания и памяти. Стек горутины стартует с ~2 КБ и растёт копированием; поток Linux по умолчанию резервирует мегабайты виртуальной памяти. Миллион горутин — реальность, миллион потоков — нет.
- Знание о семантике программы. Рантайм видит, что горутина блокируется на канале, мьютексе или сетевом IO. Вместо блокировки потока он паркует горутину и отдаёт P другой работе. Сетевой ввод-вывод уходит в netpoller (epoll/kqueue/IOCP): тысячи «блокирующих» чтений держатся на одном потоке.
- Блокирующие syscall’ы обрабатываются отдельно. Когда M уходит в долгий syscall (файловый IO, cgo), фоновый
sysmonотбирает у него P и отдаёт другому M, так что остальные горутины продолжают исполняться. - Интеграция с GC. Планировщик умеет останавливать горутины в безопасных точках, помогать GC (mark assist) и вытеснять горутины асинхронно сигналами (с Go 1.14) — без этого цикл без вызовов функций мог зависать на весь STW.
Что честно сказать про минусы: у Go-планировщика нет приоритетов и нет реалтайм-гарантий, справедливость приблизительная (глобальная очередь проверяется примерно раз в 61 тик, есть механика против голодания), и он ничего не знает про NUMA и топологию кешей — в этом ОС-планировщик сильнее.
Какие технологические преимущества экосистемы Go вы можете назвать?
Заголовок раздела «Какие технологические преимущества экосистемы Go вы можете назвать?»Коротко. Единый тулчейн из коробки (сборка, тесты, бенчмарки, профилирование, трассировка, race-детектор, фаззинг, go vet, форматтер), модули с воспроизводимыми сборками и checksum-базой, кросс-компиляция в один статический бинарник, обещание обратной совместимости Go 1, сильная стандартная библиотека (полноценный HTTP/TLS/крипто-стек) и большая экосистема инфраструктурного софта (Kubernetes, Docker, Prometheus, etcd, Terraform), написанного на Go.
Глубже. Детали, отличающие ответ от общих слов:
- Совместимость. Код 2013 года собирается сегодняшним компилятором. Обновление версии Go — это обычно бамп строки в Dockerfile; в связке с директивой
go 1.xвgo.modиtoolchain(1.21+) язык умеет менять поведение по версии модуля, не ломая старый код (GODEBUG-совместимость). - Модули.
go.sum+ проксиproxy.golang.org+sum.golang.orgдают воспроизводимость и защиту от подмены;go mod vendor,GOFLAGS=-mod=readonly,go mod tidy -diffв CI; минимальный выбор версий (MVS) вместо SAT-решателя — предсказуемо и быстро. - Единый стиль.
gofmtубрал споры о форматировании как класс;go vetиstaticcheckловят типовые ошибки;golangci-lintсобирает всё в одну команду. - Тестирование в стандартной библиотеке.
testing, табличные тесты,t.Cleanup,testing.B,go test -race -cover -fuzz,httptest,iotest,testing/quick. Не нужно выбирать между пятью фреймворками. - Деплой.
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go buildдаёт бинарник, который кладётся вscratch/distroless-образ на десяток мегабайт, без рантайма и зависимостей — отсюда популярность в контейнерах. - Наблюдаемость.
net/http/pprofподключается одной строкой и работает в проде;runtime/metrics,expvar, OpenTelemetry-SDK. - Скорость сборки. Отсутствие заголовочных файлов, запрет циклических импортов и кеш сборки дают компиляцию больших проектов за секунды — это напрямую влияет на скорость итераций и CI.
Какие технологические недостатки языка Go вы можете назвать?
Заголовок раздела «Какие технологические недостатки языка Go вы можете назвать?»Коротко. Многословная обработка ошибок без sum-типов; нулевые значения, из-за которых нельзя выразить «обязательное поле»; ловушка типизированного nil в интерфейсе; ограниченные дженерики (нет методов с параметрами типа, нет специализации, слабоватый вывод); GC без ручного контроля памяти и без арен; отсутствие иммутабельности и const-корректности; посредственная оптимизация по сравнению с LLVM-языками; дорогой и неудобный FFI (cgo); отсутствие настоящих enum’ов и проверки исчерпанности switch.
Глубже. Что стоит добавить, чтобы ответ был инженерным, а не жалобой:
- Ошибки.
if err != nil { return fmt.Errorf(...) }занимает до трети кода. Обёртки через%wиerrors.Is/Asрешают семантику, но не многословность. Предложенияcheck/handleиtryбыли отклонены; в 2024–2025 команда Go официально свернула поиск нового синтаксиса. - Нулевые значения. Нельзя отличить «поле не пришло» от «пришло 0/пустая строка/false» без указателей или
sql.Null-обёрток. В JSON это чинится указателями,json.RawMessageили (с 1.24)omitzeroна выходе. - Дженерики. Нельзя объявить метод с собственным параметром типа (
func (s *Set[T]) Map[U any](...)не компилируется), нет частичной специализации, нет ковариантности контейнеров, нет ограничения «тип с полем». Компилятор использует GC-shape stenciling, поэтому дженерик-код на указателях местами медленнее мономорфизированного. - Память. Нет размещения в стеке по требованию, нет пользовательских аллокаторов, эксперимент с аренами приостановлен; тонкая настройка — только
GOGC,GOMEMLIMIT,sync.Poolи укрупнение объектов. - Компилятор. Ставка на скорость сборки: слабее инлайнинг, нет автовекторизации/SIMD-intrinsics, нет LTO; PGO появилось только в 1.21 и даёт единицы процентов.
- Пакеты и зависимости. Запрет циклических импортов дисциплинирует, но иногда заставляет плодить пакеты
types/domain; версионирование модулей с/v2в пути импорта раздражает. - Мелочи, которые бесят на практике. Нет тернарного оператора и перегрузки операторов; ошибка компиляции на неиспользуемую переменную и импорт (хорошо в CI, мешает при отладке);
struct tags— строки без проверки;time.Timeсодержит монотонные часы и==сравнивает не то, что ожидаешь;nil-мапа падает на записи, но молча читается.
Почему на Go практически не пишут расширений для других языков и динамических библиотек?
Заголовок раздела «Почему на Go практически не пишут расширений для других языков и динамических библиотек?»Коротко. Потому что Go-код тащит с собой весь рантайм: сборщик мусора, планировщик, собственные обработчики сигналов и собственные стеки. Технически -buildmode=c-shared/c-archive работают, но получившаяся .so весит мегабайты, требует инициализации рантайма, конфликтует с сигналами и потоками хоста, не выгружается через dlclose, и каждый вызов через границу стоит существенно дороже обычного. Для расширений проще C, C++, Rust или Zig, у которых нет рантайма.
Глубже. Конкретные препятствия:
- Инициализация и единственность рантайма. При загрузке библиотеки поднимается Go-рантайм со своими потоками (sysmon, GC-воркеры). Две разных Go-библиотеки в одном процессе — два рантайма, и это не поддерживаемая конфигурация.
- Сигналы. Go устанавливает свои обработчики (
SIGSEGVдля роста стека и nil-разыменований,SIGURGдля асинхронного вытеснения). В чужом процессе это конфликтует с обработчиками хоста; отсюдаGOTRACEBACK/signal.Notify-пляски иos/signal.Ignore. - Правила передачи указателей cgo. Go-указатель нельзя долго хранить на стороне C: GC перемещает стеки и не видит внешних ссылок. Приходится работать через
runtime/cgo.Handle(Go 1.17+) и копировать данные. - Стоимость вызова. Переход Go↔C переключает на системный стек и уведомляет планировщик; порядок сотен наносекунд против единиц наносекунд у нативного вызова. Для «мелких частых» API расширений это фатально.
- Выгрузка невозможна.
dlcloseдля Go-библиотеки не поддержан; хост-процессы, ожидающие горячей перезагрузки плагинов, обломаются. Собственный пакетpluginв Go — с теми же ограничениями (только Linux/macOS, ровно те же версии Go и зависимостей, без выгрузки). - Размер. Минимальная Go-библиотека — единицы мегабайт против десятков килобайт у C.
- Плюс контекст экосистем. Питоновские/нодовые расширения обычно пишут ради скорости и работы с GIL/event loop, где лишний GC и своё планирование только мешают.
Где это всё же делают: WASM (GOOS=wasip1/js), CLI-утилиты вместо библиотек, gRPC/HTTP-сайдкар вместо линковки в процесс, редкие случаи вроде библиотеки для интеграции с существующим C-кодом (например, libgit2-подобные обёртки наоборот) — но это исключения.
Go - императивный или декларативный? А в чем разница?
Заголовок раздела «Go - императивный или декларативный? А в чем разница?»Коротко. Go — императивный язык: программа описывает как пошагово менять состояние (присваивания, циклы, ветвления). Декларативный подход описывает что нужно получить, оставляя способ реализации исполнителю — SQL, HTML, Prolog, конфиги Terraform, чистый Haskell. Go содержит небольшие декларативные вкрапления (struct tags, go:generate, шаблоны text/template, DSL-подобные билдеры), но парадигма — императивная, процедурная с элементами ООП через композицию.
Глубже. Различие полезно сформулировать так: в императивном коде вы отвечаете за порядок шагов и за состояние; в декларативном вы описываете цель или правила, а стратегия вычисления скрыта. Пример: SELECT ... WHERE ... — вы не пишете циклы обхода индексов, за вас это делает планировщик; в Go эквивалент — явный цикл по слайсу с if.
В Go нет ключевых признаков функционально-декларативного стиля: нет ленивости, нет иммутабельности по умолчанию, нет сопоставления с образцом, нет чистоты (побочные эффекты повсюду), функции хоть и first-class, но синтаксического сахара под композицию мало. Дженерики и итераторы (iter.Seq с 1.23, slices/maps с функциями Map-подобного назначения) немного двигают код в функциональную сторону, но команда Go сознательно не тащит map/filter/reduce в стандартную библиотеку как основной стиль.
Полезно добавить, что деление не бинарное: язык может поддерживать несколько парадигм, а декларативность в проектах на Go чаще приходит извне — из конфигурации (YAML, protobuf-схемы), кодогенерации и деклараций в тегах структур.
Почему треды в Go - легковесные?
Заголовок раздела «Почему треды в Go - легковесные?»Коротко. Потому что «треды» в Go — это не потоки ОС, а горутины: структуры данных рантайма с маленьким растущим стеком (старт ~2 КБ против мегабайтов у потока), которые планировщик Go переключает в пространстве пользователя за десятки наносекунд, без входа в ядро.
Глубже. Три источника лёгкости:
- Стек. Стек горутины выделяется в куче и растёт по мере надобности: пролог функции проверяет границу стека, при нехватке вызывается
morestack, рантайм выделяет вдвое больший сегмент и копирует старый (со времён Go 1.4 — непрерывный стек вместо segmented, поэтому нет «hot split»-проблемы). Указатели внутрь стека корректируются при копировании — это возможно потому, что рантайм точно знает карту указателей. Стек может и уменьшаться при GC. Потоку ОС же приходится резервировать фиксированный большой стек заранее. - Планирование в userspace. Переключение — это
gogo-переход: сохранить SP/PC/несколько регистров вg.sched, восстановить из другойg. Никакого syscall, смены адресного пространства и обхода планировщика ядра. Создание горутины — аллокация структурыg(часто из свободного списка P) и постановка в локальную очередь, порядка сотен наносекунд, против десятков микросекунд наclone(). - Кооперация с IO. Горутина, «заблокированная» на сети, на самом деле снята с потока и ждёт события netpoller’а; поток продолжает исполнять других. Поэтому 100k «заблокированных» горутин потребляют память, но не потоки.
Важная оговорка для честного ответа: лёгкие — не значит бесплатные. Каждая горутина — это минимум пара килобайт плюс структура g, и утечка горутин остаётся самой частой утечкой памяти в Go-сервисах. Плюс горутина не даёт изоляции: паника в одной роняет процесс, а runtime.Goexit/восстановление паники надо предусматривать вручную.
Почему иногда говорят, что в Go nil имеет тип?
Заголовок раздела «Почему иногда говорят, что в Go nil имеет тип?»Коротко. Само по себе nil — предопределённый идентификатор без типа (untyped nil), у него нет типа по умолчанию, поэтому x := nil не компилируется. Но при присваивании конкретному типу получается типизированный nil: (*T)(nil), nil-слайс, nil-мапа. И когда такой типизированный nil кладут в интерфейс, интерфейс перестаёт быть nil — потому что его пара (тип, значение) имеет непустой тип.
Глубже. Каноническая ловушка:
type MyErr struct{}
func (e *MyErr) Error() string { return "boom" }
func do() error { var e *MyErr // nil указатель // ...ошибки не произошло, но мы возвращаем e return e // интерфейс error = (*MyErr, nil) — НЕ nil!}
func main() { if err := do(); err != nil { fmt.Println("сработает:", err == nil) // "сработает: false" }}Правильно — возвращать литеральный nil, а не переменную конкретного типа, либо не объявлять типизированную переменную ошибки вовсе. Проверить «внутренний» nil можно рефлексией (reflect.ValueOf(err).IsNil()), но это лечение симптома. Линтер nilness из go vet-набора и staticcheck частично ловят такие случаи.
Второй смысл фразы «nil имеет тип»: nil-значения разных типов ведут себя по-разному, и это часть контракта. Nil-указатель на структуру можно передавать в метод с pointer receiver и он корректно отработает, если не разыменовывает поля (классика — nil-дерево с методом func (t *Tree) Sum() int { if t == nil { return 0 } ... }). Nil-слайс полностью пригоден для append, len, range. Nil-мапу можно читать, но не писать. Из nil-канала чтение и запись блокируются навсегда (используется, чтобы «выключить» ветку select). Nil-функция паникует при вызове. Итого: нулевое значение зависит от типа, и «nil» — это шесть разных нулевых значений, а не одно.
Какой у вас любимый логгер?
Заголовок раздела «Какой у вас любимый логгер?»Коротко. Ответ-каркас: с Go 1.21 в стандартной библиотеке есть log/slog — структурированное логирование с уровнями и хендлерами, и по умолчанию я беру его; если нужен максимум производительности в горячем пути — zap (или zerolog) с нулевыми аллокациями. Главное в ответе — не название, а обоснование: структурированность, уровни, контекст запроса, отсутствие аллокаций при отключённом уровне.
Глубже. Что стоит проговорить:
- Почему
slog. Стандарт, не тянет зависимость,slog.Handlerпозволяет подменить бэкенд (JSON в проде, текст локально), совместим с legacylogчерезslog.SetDefault. Естьslog.LogAttrsдля варианта без аллокаций иLogger.Enabled(ctx, level)для дорогих вычислений. Есть адаптеры кzap/zerolog, так что миграция не односторонняя. - Почему иногда
zap/zerolog. На пути с сотнями тысяч записей в секунду разница ещё ощутима:zap(core-API, не sugared) иzerologпишут в буфер без рефлексии и без аллокаций; уzapесть sampling — защита от лавины одинаковых строк при инциденте. - Практики, которые важнее выбора библиотеки. Один логгер, прокинутый через DI (глобальный — только как fallback); контекст запроса (
trace_id,request_id) добавляется черезlogger.With(...), лежащий вcontext.Context; уровни используются осмысленно (error— то, на что заводят алерт;warn— деградация;info— бизнес-события, а не отладка); никаких PII, токенов, тел запросов; JSON в проде для парсинга в Loki/ELK; логирование ошибки ровно один раз, на верхнем уровне, а не на каждом кадре стека; отсутствие логов в горячем цикле. - Связь с трассировкой. Логи-метрики-трейсы: логи коррелируются с трейсом по
trace_id(OpenTelemetry), и часто дешевле поставить спан, чем строку лога.
Как вы применяете пакеты internal?
Заголовок раздела «Как вы применяете пакеты internal?»Коротко. internal — механизм видимости уровня компилятора: пакет с internal в пути импорта можно импортировать только из кода, чей путь лежит внутри дерева, начинающегося с родителя этого internal. Я кладу туда всё, что не является публичным API: доменную логику сервиса, адаптеры к БД и брокерам, конфиг, — а наружу оставляю минимум (например, сгенерированные клиенты в pkg/ или api/).
Глубже. Правило точное: example.com/m/internal/foo доступен из example.com/m/... и недоступен извне; вложенные internal сужают область ещё сильнее (example.com/m/service/a/internal/x виден только внутри service/a). Нарушение — ошибка компиляции, а не соглашение, поэтому это единственный способ в Go реально запретить чужой импорт.
Как это используют:
- В сервисах. Практически весь код в
internal/, аcmd/<binary>/main.go— тонкая обвязка. Так вы гарантируете, что никто не «подцепит» ваши внутренности как библиотеку и не превратит рефакторинг в breaking change. - В библиотеках. Скрыть вспомогательные пакеты (
internal/pool,internal/parse), сохранив возможность менять их без версии/v2. Именно так устроена стандартная библиотека (internal/abi,internal/runtime/maps) иgolang.org/x/.... - Как границы модулей внутри монорепо. Вложенные
internalв поддиректориях-доменах (internal/billing/internal/...) дают физически принудительные границы между командами — намного надёжнее, чем договорённость «не импортировать чужое». - Тестирование. Тесты внутри пакета лежат в том же пакете и видят неэкспортируемое; для
internal-пакетов ничего специального не требуется. Экспорт для тестов делают через файлexport_test.go.
Отдельно про «pkg/ vs internal/»: pkg/ — просто соглашение из community-layout, компилятор о нём ничего не знает; internal/ — часть языка. Мой подход: по умолчанию internal/, а в pkg//корень выносится только то, что мы осознанно готовы поддерживать как публичный API.
Какой у вас любимый линтер?
Заголовок раздела «Какой у вас любимый линтер?»Коротко. golangci-lint — не столько линтер, сколько агрегатор: параллельно запускает десятки анализаторов по одному AST/типизированному графу, кешируется и удобно встраивается в CI. Внутри него база — go vet, staticcheck (SA/S/ST), errcheck, revive, ineffassign, gocritic, gosec.
Глубже. Что имеет смысл сказать про набор и процесс:
- Обязательный минимум:
govet(в том числеprintf,copylocks,lostcancel,nilness),staticcheck,errcheck(незамеченные ошибки — источник большинства инцидентов),ineffassign,bodycloseиsqlclosecheck(утечки соединений),rowserrcheck,contextcheck/noctx,errorlint(правильные%w,errors.Isвместо==),exhaustiveдля enum-подобныхswitch,gosecв сервисах с внешним вводом. - Что чаще отключают: излишне придирчивые
lll,funlen,gochecknoglobals,wsl— они генерируют шум и споры. - Процесс важнее списка. Конфиг
.golangci.ymlв репозитории, тот же самый запуск локально и в CI (в идеале — черезgo toolизgo.mod, чтобы версия совпадала),--new-from-rev=origin/mainпри внедрении в старый проект (проверяются только новые строки, не нужно чинить 5000 замечаний разом),nolintlint, чтобы//nolintбез объяснения не расползался. - За пределами линтеров:
gofumpt/gciдля формата и импортов,go vet -raceв тестах,govulncheckдля известных CVE в зависимостях (это не линтер, но в том же CI-шаге).
Что такое сериализация? Зачем она нужна?
Заголовок раздела «Что такое сериализация? Зачем она нужна?»Коротко. Сериализация — преобразование значения в памяти в последовательность байтов, пригодную для передачи или хранения, а десериализация — обратная операция. Нужна везде, где данные покидают процесс: сетевые вызовы (HTTP/gRPC), очереди, кеши, файлы и БД, межпроцессное взаимодействие, снапшоты состояния.
Глубже. Ключевая проблема, ради которой сериализация вообще существует: в памяти значение — это граф объектов с указателями, выравниванием и представлением, зависящим от платформы и версии компилятора. Указатель бессмысленен вне адресного пространства, поэтому граф нужно «уплостить» в самоописывающийся или схемный формат.
Что важно проговорить:
- Форматы и их выбор. JSON — человекочитаемый, повсеместный, но многословный и медленный; Protobuf/gRPC — компактный, схемный, быстрый, с контролируемой эволюцией; MessagePack/CBOR — бинарный JSON; Avro — схема в реестре, популярна в Kafka;
encoding/gob— только между Go-программами, зато умеет типы Go «как есть»; текстовые YAML/TOML — для конфигов, не для трафика. - Эволюция схемы. Главный практический критерий. Protobuf даёт правила совместимости (не переиспользовать номера полей, не менять тип,
reserved), JSON — по факту «неизвестные поля игнорируются» (в Go можно ужесточитьDecoder.DisallowUnknownFields). - Специфика Go.
encoding/jsonработает через рефлексию: экспортируемые поля, тегиjson:"name,omitempty", интерфейсыjson.Marshaler/Unmarshalerиencoding.TextMarshaler. Подводные камни: числа по умолчанию декодируются вfloat64(для больших int64 теряется точность — лечитсяDecoder.UseNumber()),[]byteкодируется в base64,time.Time— RFC 3339, циклы указателей приводят к переполнению стека,omitemptyне умеет структуры иtime.Time(в 1.24 появилсяomitzero), приватные поля не сериализуются. При высоких требованиях к скорости — кодогенерация (easyjson,ffjson) или быстрые парсеры (goccy/go-json,bytedance/sonic); в Go 1.25 доступен экспериментальныйencoding/json/v2подGOEXPERIMENT=jsonv2. - Безопасность. Десериализация недоверенного ввода — ограничение размера (
http.MaxBytesReader), защита от «zip-бомб» и глубокой вложенности, никакого исполнения кода при разборе (в Go этой проблемы, в отличие от Java/Python pickle, практически нет).
У вас есть кривой json с плавающим типом полей. Как написать десериализатор?
Заголовок раздела «У вас есть кривой json с плавающим типом полей. Как написать десериализатор?»Коротко. Объявить свой тип для проблемного поля и реализовать на нём json.Unmarshaler: посмотреть на первый значащий байт сырого фрагмента (" — строка, [ — массив, { — объект, t/f — bool, n — null, иначе число) и разобрать соответствующим образом. Так «кривизна» локализуется в одном типе, а остальная структура остаётся обычной.
Глубже. Пример для классического случая «число иногда приходит строкой, иногда числом, иногда null или пустой строкой»:
type FlexInt int64
func (f *FlexInt) UnmarshalJSON(data []byte) error { data = bytes.TrimSpace(data) if len(data) == 0 || string(data) == "null" { *f = 0 return nil } if data[0] == '"' { var s string if err := json.Unmarshal(data, &s); err != nil { return fmt.Errorf("flexint: %w", err) } s = strings.TrimSpace(s) if s == "" { *f = 0 return nil } n, err := strconv.ParseInt(s, 10, 64) if err != nil { return fmt.Errorf("flexint %q: %w", s, err) } *f = FlexInt(n) return nil } var n json.Number if err := json.Unmarshal(data, &n); err != nil { return fmt.Errorf("flexint: %w", err) } v, err := n.Int64() if err != nil { // пришло 12.0 — округляем осознанно, а не молча теряем точность fl, ferr := n.Float64() if ferr != nil { return fmt.Errorf("flexint %s: %w", n.String(), ferr) } v = int64(fl) } *f = FlexInt(v) return nil}Второй частый вид «кривизны» — поле, которое приходит то одиночным объектом, то массивом объектов. Приём тот же:
type Items []Item
func (it *Items) UnmarshalJSON(data []byte) error { data = bytes.TrimSpace(data) if len(data) > 0 && data[0] == '[' { var arr []Item if err := json.Unmarshal(data, &arr); err != nil { return err } *it = arr return nil } var one Item if err := json.Unmarshal(data, &one); err != nil { return err } *it = Items{one} return nil}Что ещё стоит упомянуть:
- Если типы «плавают» по всей структуре и схемы нет вообще — разбирайте в
map[string]anyсDecoder.UseNumber()(иначе все числа станутfloat64) и валидируйте вручную, либо используйтеjson.RawMessageдля отложенного разбора отдельных веток. - Метод обязан быть на указателе (
func (f *FlexInt) UnmarshalJSON), иначе он не будет вызван для поля структуры. - Внутри
UnmarshalJSONнельзя вызыватьjson.Unmarshal(data, f)на своём же типе — получите бесконечную рекурсию; используют трюк с локальным типом-двойником:type alias FlexInt; var a alias; json.Unmarshal(data, &a). UnmarshalJSONполучает копию сырых байтов только на время вызова: если хотите сохранить их, копируйте (append([]byte(nil), data...)), это же касаетсяjson.RawMessageв потоковом декодере.- Симметрично полезно реализовать
MarshalJSON, чтобы round-trip был стабильным, и покрыть всё табличными тестами со всеми встречавшимися формами входа, включая мусорные.
Чем отличается работа с указателями в Go по сравнению с C++? Какие ограничения и почему введены?
Заголовок раздела «Чем отличается работа с указателями в Go по сравнению с C++? Какие ограничения и почему введены?»Коротко. В Go указатель — это только «адрес значения известного типа»: нет арифметики указателей, нет ручного delete, нет приведения между несовместимыми типами указателей, нет разницы между указателем и ссылкой, нет указателей на члены и функций-деструкторов. Всё это убрано, чтобы GC мог точно знать, где в памяти лежат указатели, и чтобы исключить целый класс UB — висячие указатели, выход за границы, double free.
Глубже. По пунктам:
- Арифметика.
p++/p+1запрещены. Обход массива — через индексы и слайсы с проверкой границ. Причина двойная: точный (precise) GC должен уметь отличить указатель от числа и не может допустить «указателей в середину чужого объекта»; плюс это убирает переполнения буфера. Обойти можно черезunsafe.Pointer/unsafe.Add, но там действуют жёсткие правила (см. документациюunsafe), и компилятор проверяет часть из них при-d=checkptr. - Освобождение. Нет
delete/free: время жизни определяет GC. Следствие — нет use-after-free и double free, но нет и детерминированного разрушения, то есть нет RAII. Освобождение внешних ресурсов делают черезdefer f.Close(), а не через деструктор. - Взятие адреса локальной переменной безопасно.
func f() *T { var t T; return &t }в C++ — классическая ошибка, в Go — норма: escape-анализ переносит переменную в кучу. Обратная сторона — вы не управляете размещением, а лишние «утечки в кучу» ловите через-gcflags=-m. - Nil. Разыменование nil — паника с трассировкой, а не UB (реализовано через ловлю
SIGSEGVрантаймом). Паника восстанавливаема черезrecover, хотя восстанавливать её обычно не следует. - Что не адресуемо. Нельзя взять адрес элемента мапы (
&m[k]) — при росте мапа переносит данные; нельзя взять адрес результата функции или константы напрямую. Элемент слайса адресуем, потому что backing array не переезжает без ведома. - Типизация. Нет
reinterpret_cast; конвертировать*Aв*Bможно только черезunsafe.Pointer, соблюдая правила совместимости размеров/выравнивания.uintptr— просто число: сохранённый в нём адрес не удерживает объект от сборки и не корректируется (стеки перемещаются!), поэтому «указатель в uintptr на потом» — гарантированный баг. - Нет ссылок. В C++ есть
T&иconst T&; в Go только значение или указатель, иconst-корректности нет вовсе — неизменяемость обеспечивается дисциплиной и копированием. - cgo-правила. Go-указатель нельзя хранить в C-памяти дольше вызова; для этого есть
runtime/cgo.Handle.
Практический вывод, который стоит озвучить: указатель в Go используют для трёх вещей — изменяемость получателя метода, избежание копирования крупных структур и выражение «значение может отсутствовать». Всё остальное лучше делать значениями.
Расскажите про cgo. Когда его использование оправдано, а когда лучше искать альтернативы? Был ли у вас опыт работы с cgo?
Заголовок раздела «Расскажите про cgo. Когда его использование оправдано, а когда лучше искать альтернативы? Был ли у вас опыт работы с cgo?»Коротко. cgo — механизм вызова C-кода из Go (псевдопакет "C", комментарий-преамбула с C-кодом и директивами #cgo CFLAGS/LDFLAGS). Оправдан, когда нужной функциональности нет и не будет в Go: зрелые C-библиотеки (SQLite, FFmpeg, librdkafka, OpenSSL/HSM, драйверы устройств, ML-рантаймы). Не оправдан ради мелких частых вызовов и там, где есть чистый Go-аналог: цена вызова, поломанная кросс-компиляция, статическая линковка и усложнение сборки обычно перевешивают.
Глубже. Что именно ломается:
- Производительность вызова. Переход Go↔C переключает горутину на системный стек и сообщает планировщику, что M уходит в «syscall»; это порядка сотен наносекунд, плюс запрет инлайна и потеря оптимизаций. Для API с миллионами мелких вызовов это убивает всю выгоду. С Go 1.23 есть
#cgo noescapeи#cgo nocallback, которые снижают накладные расходы (аргументы не эскейпятся в кучу), но не убирают их. - Планировщик и потоки. Долгий C-вызов занимает поток ОС; при большом числе параллельных вызовов рантайм плодит потоки. C-код, который сам создаёт потоки или ставит обработчики сигналов, конфликтует с рантаймом Go.
- Память и GC. Действуют правила передачи указателей: Go-память, переданная в C, не должна содержать Go-указателей и не должна храниться после возврата. Память, выделенная
C.malloc, GC не видит — освобождать вручную (defer C.free(unsafe.Pointer(p))). Утечки на границе — самая частая проблема. - Сборка и дистрибуция. Нужен C-тулчейн;
CGO_ENABLED=0(а значитFROM scratch, простая кросс-компиляция и полностью статический бинарник) становится недоступен, если только не собирать со статическими библиотеками и musl. Сборка резко замедляется. - Диагностика. Race-детектор и pprof не видят C-часть; паники не проходят через границу,
SIGSEGVв C-коде роняет процесс без внятного Go-стека; отладка требует gdb/lldb.
Альтернативы: чистые Go-порты (modernc.org/sqlite — транспилированный SQLite без cgo, pure-Go реализации крипты и парсеров), отдельный процесс/сайдкар с IPC (gRPC, unix socket, shared memory) — так изолируется и падение C-кода, вызов внешней утилиты через os/exec, WASM-модуль (wazero) как безопасная песочница для чужого кода.
Каркас ответа про личный опыт: назвать конкретную библиотеку и задачу («подключали librdkafka через confluent-kafka-go, потому что нужны были транзакции и точные метрики, которых тогда не было в segmentio/kafka-go»), затем что именно стало больно (сборка образа, статическая линковка, отсутствие профилирования C-части), затем как это решили (multi-stage Dockerfile, зафиксированная версия библиотеки, обёртка с ограничением конкурентности, тесты на утечки через runtime.NumGoroutine и RSS). Если опыта не было — сказать честно и описать, при каких условиях вы бы на cgo пошли и как бы измеряли цену.
Какие минусы Go вы для себя отметили после перехода с C++? Чего не хватает или что вызывает дискомфорт?
Заголовок раздела «Какие минусы Go вы для себя отметили после перехода с C++? Чего не хватает или что вызывает дискомфорт?»Коротко. После C++ обычно не хватает RAII и детерминированного разрушения, полноценных шаблонов (в Go нет специализации, методов с параметрами типа, перегрузки операторов, constexpr), const-корректности, контроля над размещением памяти и аллокаторами, move-семантики, а также предельной оптимизации: нет SIMD-intrinsics, слабее инлайнинг, нет LTO. Плюс многословная обработка ошибок вместо исключений и отсутствие sum-типов.
Глубже. Что стоит проговорить, показывая понимание компромисса:
- RAII → defer.
deferпривязан к функции, а не к области видимости, поэтому в длинных функциях и циклах приходится выделять вспомогательные функции; нет деструкторов, значит владение ресурсом выражается только дисциплиной (Close()и документированный контракт). Взамен нет проблем с порядком разрушения и исключениями в деструкторах. - Шаблоны → дженерики. Дженерики Go намеренно слабее: нет специализации и рекурсивной инстанциации, нет non-type параметров, нет вывода из возвращаемого типа, нет ограничения «тип с полем». Компиляция идёт по GC-shape stenciling — один инстанс на класс схожих типов, что иногда дороже мономорфизации. Взамен: нет template-метапрограммирования как языка внутри языка и нет часовых сборок.
- Память. Нет
placement new, кастомных аллокаторов, арен (эксперимент приостановлен), объектных пулов на уровне языка (sync.Pool— только кеш с потерями при GC), нет контроля выравнивания и layout (кроме порядка полей вручную). Плюс паузы GC, пусть и субмиллисекундные. - Оптимизация. Отсутствие intrinsics вынуждает писать критичные куски на ассемблере Plan 9 — заметный откат по удобству. PGO помогает, но потолок ниже, чем у LLVM.
- Мелочи. Нет перегрузки операторов (математические типы выглядят громоздко), нет тернарника, нет неизменяемых структур, нет
[[nodiscard]], нет пространств имён внутри пакета,interface{}/anyтам, где в C++ был бы шаблон. - Что взамен нравится. Секунды на сборку вместо минут; отсутствие UB как повседневной реальности; один способ форматирования; готовый race-детектор и профайлер; конкурентность, которая не требует библиотеки поверх библиотеки. Именно эта часть ответа показывает, что вы не «ноете», а взвешиваете.
В чем разница между make и new ? Когда что использовать?
Заголовок раздела «В чем разница между make и new ? Когда что использовать?»Коротко. См. выше два ответа про make/new. Правило выбора простое: make — когда нужен рабочий слайс, мапа или канал; new (а на практике &T{}) — когда нужен указатель на обнулённое значение любого другого типа. Если вы написали new для слайса/мапы/канала — почти наверняка ошибка.
Глубже. Дополню чек-листом «когда что»:
- Нужен слайс известной длины/ёмкости →
make([]T, 0, n)илиmake([]T, n); литерал[]T{}— когда важно именно не-nil пустое значение (например, чтобы JSON дал[], а неnull). - Нужна мапа → всегда
makeили литерал;var m map[K]Vоставляйте только если мапа заведомо read-only-пустая. - Нужен канал → всегда
make;var ch chan T(nil-канал) применяется осознанно, чтобы отключать веткуselect. - Нужен указатель на структуру →
&T{Field: ...}, а неnew(T)с последующими присваиваниями. - Нужен указатель на скаляр (опциональное поле в API/JSON) →
new(int)либо вспомогательный дженерикfunc ptr[T any](v T) *T { return &v }. - Нужен указатель на синхронизационный примитив → объявляйте значением (
var mu sync.Mutex) и передавайте указатель; копировать такие типы нельзя (ловитсяgo vetчерезcopylocks).
Как в Go работает конструкция oneof ?
Заголовок раздела «Как в Go работает конструкция oneof ?»Коротко. В самом языке Go нет oneof/union-типов. Речь обычно о protobuf: генератор protoc-gen-go превращает oneof в поле-интерфейс с неэкспортируемым методом (sealed interface) и по одному wrapper-типу на каждый вариант; разбирают его через switch v := msg.GetPayload().(type). Тот же приём — «интерфейс с приватным методом + type switch» — идиоматичный способ вручную выразить sum-тип в Go.
Глубже. Для схемы
message Event { oneof payload { string text = 1; int64 code = 2; }}генерируется примерно такое (имена зависят от версии генератора): интерфейс isEvent_Payload с неэкспортируемым методом, типы Event_Text{Text string} и Event_Code{Code int64}, поле Payload isEvent_Payload и геттеры GetText()/GetCode(), возвращающие нулевое значение, если установлен другой вариант. Использование:
switch p := ev.GetPayload().(type) {case *pb.Event_Text: handleText(p.Text)case *pb.Event_Code: handleCode(p.Code)case nil: // поле не установлено — это валидное состояние oneofdefault: return fmt.Errorf("unknown payload %T", p)}Важные детали: oneof допускает состояние «ничего не установлено» (nil), и его надо обрабатывать явно; установка одного варианта автоматически сбрасывает другой; неэкспортируемый метод интерфейса не даёт объявить свой вариант в чужом пакете — это и есть «sealed». В опаковом API protobuf-go (появился в v1.36) вместо type switch используются генерируемые Which<Oneof>() и Has/Set-аксессоры — если работаете с ним, стоит упомянуть.
Ручной аналог без protobuf:
type Shape interface{ isShape() }
type Circle struct{ R float64 }type Rect struct{ W, H float64 }
func (Circle) isShape() {}func (Rect) isShape() {}Минус подхода — компилятор не проверяет исчерпанность switch, поэтому обязательна ветка default с ошибкой, а в CI — линтер exhaustive. Ещё минус — такие типы плохо сериализуются в JSON «сами по себе», отсюда следующий блок вопросов про kind + json.RawMessage.
Что такое json.RawMessage и когда его использование оправдано?
Заголовок раздела «Что такое json.RawMessage и когда его использование оправдано?»Коротко. json.RawMessage — это []byte с методами MarshalJSON/UnmarshalJSON, которые ничего не делают: при разборе туда кладётся сырой, ещё не разобранный фрагмент JSON, при сериализации он вставляется как есть. Оправдан, когда разбор фрагмента нужно отложить (полиморфный payload, зависящий от поля kind), когда данные нужно пробросить без изменений (прокси, аудит, хранение «как пришло») или когда часть документа вы просто не хотите парсить.
Глубже. Типичное применение — конверт:
type Envelope struct { Kind string `json:"kind"` Data json.RawMessage `json:"data"`}Сначала разбирается конверт (дёшево), затем по Kind выбирается конкретный тип и json.Unmarshal(env.Data, &concrete). Это ровно тот случай, когда «двойной парсинг» превращается в «полтора»: внешний уровень разбирается один раз, внутренний — ровно один раз и сразу в правильный тип.
Второе применение — прозрачный проброс: сервис принимает документ, меняет два поля и отдаёт дальше; всё неизвестное лежит в map[string]json.RawMessage и переживает round-trip без потери порядка значений внутри фрагментов и без потери точности чисел.
Третье — производительность: если из большого документа нужны два поля, дешевле распарсить верхний уровень с RawMessage и разобрать только нужные ветки.
Важные свойства: RawMessage не валидируется при разборе внешнего документа (точнее, синтаксис проверяется декодером, но структура — нет); при Marshal содержимое подставляется как есть, поэтому мусор внутри даст невалидный JSON на выходе (json.Valid в помощь); поле должно быть типа json.RawMessage, а не *json.RawMessage, если вы не хотите отличать «отсутствует» от «null»; и главное — байты, полученные в UnmarshalJSON, действительны только на время вызова, поэтому при сохранении копируйте.
Какие подводные камни нужно учесть?
Заголовок раздела «Какие подводные камни нужно учесть?»Коротко. В контексте предыдущего вопроса (полиморфный JSON через kind + RawMessage) главные грабли: срок жизни байтов RawMessage, отсутствие валидации содержимого, «двойной парсинг» при наивной реализации, неизвестные значения kind, гонки при регистрации типов в глобальной карте, рекурсия в кастомном UnmarshalJSON и нулевые значения, неотличимые от отсутствующих полей.
Глубже. Список подробнее:
- Владение байтами.
json.RawMessageв структуре, заполненной черезjson.Unmarshal([]byte), ссылается на срез входного буфера; если буфер переиспользуется (sync.Pool,bufio,io.ReadAllв цикле) — данные испортятся. Копируйте:raw = append(json.RawMessage(nil), raw...). - Валидация. Проверяйте, что
Dataне пустой и валиден, до того как класть его в очередь или БД. - Ограничение размера и глубины. Недоверенный ввод — только через
http.MaxBytesReader; глубоко вложенный JSON может съесть стек и CPU. - Числа. По умолчанию в
anyчисла становятсяfloat64—int64больше 2^53 теряет точность. ИспользуйтеDecoder.UseNumber()или конкретные типы. - Регистр и неизвестные поля. Матчинг имён полей в
encoding/jsonрегистронезависимый ({"KIND": ...}попадёт вKind), а неизвестные поля молча игнорируются — включайтеDecoder.DisallowUnknownFields(), если контракт строгий. - Рекурсия в
UnmarshalJSON. Вызовjson.Unmarshal(data, v)внутри метода того же типа — бесконечная рекурсия и переполнение стека. Спасает локальный тип-двойник. - Указательный получатель.
UnmarshalJSONобязан быть на*T, иначе не вызовется. Симметрично,MarshalJSONна*Tне сработает для значения, положенного в интерфейс не по указателю. - Нулевые значения.
omitemptyне отличает «0» от «нет поля»; для строгих API — указатели илиjson.RawMessage, а на выходе —omitzero(Go 1.24). - Реестр типов. Карта конструкторов должна заполняться только в
init()/при старте и дальше быть read-only, иначе конкурентная запись в мапу уронит процесс. Если регистрация возможна в рантайме —sync.RWMutexилиsync.Map. - Ошибки должны быть информативными. Оборачивайте:
fmt.Errorf("decode payload kind=%q: %w", kind, err)— иначе в проде вы получите «unexpected end of JSON input» без контекста. - Тесты. Табличные тесты со всеми вариантами
kind, с неизвестнымkind, сnull, с пустымdata, с мусором; плюсgo test -fuzzна декодере — он отлично находит паники.
Как избежать двойного парсинга JSON?
Заголовок раздела «Как избежать двойного парсинга JSON?»Коротко. Разбирать конверт один раз, а payload оставлять в json.RawMessage и декодировать ровно один раз уже в конкретный тип. Наивный вариант «сначала распарсить всё в map[string]any, узнать kind, потом распарсить весь документ заново в нужную структуру» — как раз двойной парсинг, и его убирает конверт.
Глубже. Каноническая реализация — кастомный UnmarshalJSON на обёртке, который скрывает двухшаговость от вызывающего кода:
type Payload interface{ Kind() string }
type Message struct { ID string Payload Payload}
type wireMessage struct { ID string `json:"id"` Kind string `json:"kind"` Payload json.RawMessage `json:"payload"`}
func (m *Message) UnmarshalJSON(data []byte) error { var w wireMessage if err := json.Unmarshal(data, &w); err != nil { return fmt.Errorf("message envelope: %w", err) } m.ID = w.ID
newPayload, ok := lookup(w.Kind) if !ok { return fmt.Errorf("unknown kind %q", w.Kind) } p := newPayload() if len(w.Payload) > 0 && string(w.Payload) != "null" { if err := json.Unmarshal(w.Payload, p); err != nil { return fmt.Errorf("payload kind=%q: %w", w.Kind, err) } } m.Payload = p return nil}Здесь верхний уровень сканируется один раз, payload — тоже один раз. Формально сканер пробегает байты payload дважды (при поиске границ фрагмента и при собственно разборе), но «дважды строить объекты» — а это и есть дорогая часть — не приходится.
Другие приёмы:
- Потоковый разбор.
json.DecoderсToken()позволяет прочитать полеkindдо payload и дальше декодировать нужный тип напрямую из потока (dec.Decode(&concrete)), не буферизуя весь документ. Работает, еслиkindв документе идёт раньше payload — а это в общем случае не гарантировано, поэтому конверт надёжнее. - Схемные форматы. Если контроль над протоколом ваш — protobuf
oneofрешает проблему на уровне схемы: тег поля уже кодирует вариант, вторичного разбора нет вовсе. - Быстрые парсеры.
bytedance/sonic,goccy/go-json,jsoniterдают ускорение без изменения структуры кода;easyjsonгенерирует код и убирает рефлексию совсем. - Не парсить лишнее. Если 90% документа не нужны — оставьте эти ветки
json.RawMessageили вовсе не объявляйте поля.
Как поступить с неизвестным kind ?
Заголовок раздела «Как поступить с неизвестным kind ?»Коротко. Решение зависит от контракта: для строгого внутреннего протокола — вернуть ошибку и отправить сообщение в DLQ; для эволюционирующего внешнего API — принять сообщение, сохранить payload как json.RawMessage в «неизвестном» варианте, залогировать/зафиксировать метрику и пропустить дальше, не падая. Молча игнорировать без метрики — худший вариант: вы узнаете о рассинхронизации версий только от пользователей.
Глубже. Практическая схема для брокерных консьюмеров и HTTP-API:
type UnknownPayload struct { KindValue string Raw json.RawMessage}
func (u UnknownPayload) Kind() string { return u.KindValue }Логика: если kind неизвестен — не ошибка парсинга, а бизнес-решение. Варианты по ситуации:
- Fail fast (внутренний протокол, обе стороны деплоятся вместе): ошибка +
nackбез ретрая + DLQ. Так рассинхронизация схем обнаруживается сразу. - Forward compatibility (публичный API, мобильные клиенты, долгоживущие очереди): принять, сохранить raw, инкрементировать
unknown_kind_total{kind="..."}, обработать как no-op. Это единственный способ выкатывать продюсера раньше консьюмера. - Прокси/роутер: пробросить как есть, ничего не интерпретируя.
Что важно в любом варианте: метрика с лейблом kind (с осторожностью — лейбл из внешнего ввода это риск кардинальности, ограничьте allowlist’ом или числом уникальных значений), лог уровня warn с сэмплированием, алерт на ненулевую скорость роста, и явный тест на этот путь. И отдельно — не делайте panic на неизвестном kind: чужие данные не должны ронять ваш процесс.
Как организовать регистрацию новых типов через init() или карту конструкторов?
Заголовок раздела «Как организовать регистрацию новых типов через init() или карту конструкторов?»Коротко. Завести в базовом пакете реестр map[string]func() Payload, функцию Register(kind, ctor) и lookup(kind); конкретные типы регистрируются в своих пакетах из init(), а базовый пакет о них ничего не знает. Заполнение — только на старте, после чего карта читается конкурентно без блокировок; если регистрация возможна в рантайме — sync.RWMutex или sync.Map.
Глубже. Реестр:
package payload
import ( "encoding/json" "fmt" "sync")
type Payload interface{ Kind() string }
var ( mu sync.RWMutex registry = make(map[string]func() Payload))
// Register вызывается из init() пакетов-реализаций.func Register(kind string, ctor func() Payload) { mu.Lock() defer mu.Unlock() if _, dup := registry[kind]; dup { panic(fmt.Sprintf("payload: duplicate kind %q", kind)) } registry[kind] = ctor}
func New(kind string) (Payload, bool) { mu.RLock() defer mu.RUnlock() ctor, ok := registry[kind] if !ok { return nil, false } return ctor(), true}
func Decode(kind string, raw json.RawMessage) (Payload, error) { p, ok := New(kind) if !ok { return nil, fmt.Errorf("unknown kind %q", kind) } if err := json.Unmarshal(raw, p); err != nil { return nil, fmt.Errorf("kind %q: %w", kind, err) } return p, nil}Реализация в другом пакете:
package events
import "example.com/app/payload"
type UserCreated struct { UserID string `json:"user_id"`}
func (*UserCreated) Kind() string { return "user.created" }
func init() { payload.Register("user.created", func() payload.Payload { return new(UserCreated) })}Компромиссы, которые стоит назвать:
init()+ blank-импорт. Чтобыinit()сработал, пакет должен быть импортирован: где-то вmainпоявляется_ "example.com/app/events". Это плата за развязку. Плюс — добавление типа не трогает базовый пакет; минус — «магия», которую сложно проследить в IDE, и зависимость от факта импорта (линкер не выбросит пакет, но забытый импорт даст «unknown kind» только в рантайме).- Явная регистрация вместо
init(). Часто чище:func Registry() *payload.Registry { r := payload.New(); r.Register(...); ... }, вызываемая изmain/composition root. Тестируемо, нет глобального состояния, порядок очевиден. Я по умолчанию выбираю этот вариант, аinit()оставляю для плагинного стиля, где список реализаций не известен базовому пакету. panicна дубликат уместен именно здесь: это ошибка программиста, обнаруживаемая на старте, а не рантайм-условие.- Тестируемость. Экземпляр реестра (структура) вместо пакетной переменной позволяет в тестах собрать изолированный набор типов; глобальный реестр делает тесты зависимыми друг от друга.
- Кодогенерация как альтернатива. Список
kindможно генерировать (go:generate) из схем/protobuf — тогда компилятор проверит исчерпанность, а не рантайм.
Назовите три самых главных преимущества Go на ваш взгляд.
Заголовок раздела «Назовите три самых главных преимущества Go на ваш взгляд.»Коротко. Первое — конкурентность как часть языка и рантайма: горутины, каналы, netpoller дают простой синхронный код с производительностью асинхронного. Второе — простота и единообразие: маленькая спецификация, gofmt, обещание совместимости Go 1, из-за чего чужой код читается сразу, а онбординг занимает недели, а не месяцы. Третье — инструментарий и деплой: один статический бинарник, секундные сборки, встроенные тесты, бенчмарки, профилировщик и race-детектор.
Глубже. Ответ выигрывает, если каждый пункт подкрепить последствием для бизнеса, а не только для разработчика: конкурентность → один инстанс держит десятки тысяч соединений, а значит меньше подов и понятная утилизация; простота и совместимость → низкий bus-factor, дешёвая ротация людей между сервисами, обновление версии Go без переписывания; тулинг → инцидент в проде диагностируется профилем с живого пода за минуты, а не воспроизводится неделю. Если интервьюер хочет «три и всё», не растекайтесь — назовите три и добавьте по одной фразе.
Обобщенно расскажите про Go в ООП и как оно реализуется.
Заголовок раздела «Обобщенно расскажите про Go в ООП и как оно реализуется.»Коротко. Go поддерживает объектно-ориентированный стиль, но без классов и без наследования: инкапсуляция — на уровне пакета (экспортируемость по заглавной букве), полиморфизм — через интерфейсы, которые удовлетворяются неявно (структурная типизация), повторное использование — через композицию и встраивание. Методы объявляются отдельно от типа и могут висеть на любом именованном типе пакета, не только на структуре.
Глубже. Ключевые механизмы:
- Методы и наборы методов.
func (u User) Name() stringиfunc (u *User) SetName(s string). Набор методовTвключает только методы с value receiver, набор*T— оба. Отсюда правило: если хоть один метод меняет получателя или тип содержит мьютекс — все методы делают на указателе, и интерфейс удовлетворяет*T, а неT. - Интерфейсы неявные. Тип не объявляет, что реализует интерфейс. Следствие: интерфейс объявляют на стороне потребителя, маленьким (
io.Reader— один метод), а функции «принимают интерфейсы, возвращают структуры». Это разворачивает зависимость и убирает необходимость в «интерфейсе на каждый сервис» — антипаттернUserServiceInterfaceрядом с единственной реализацией. - Встраивание — не наследование.
type Admin struct { User }даёт продвижение полей и методов (a.Name()работает), иAdminавтоматически удовлетворяет интерфейсам, которые удовлетворяетUser. Но динамической диспетчеризации нет: еслиUser.Describe()вызываетu.Name(), аAdminопределил свойName(), вызовется всё равноUser.Name— «переопределения» в смысле virtual нет. Это ровно тот вопрос, на котором отсеивают тех, кто считает встраивание наследованием. - Инкапсуляция. Единица сокрытия — пакет, а не тип: неэкспортируемое поле видно всем файлам пакета. Отсюда практика «один пакет — один агрегат/домен», а не «один пакет — один слой на весь сервис».
- Конструкторы и инварианты. Конструкторов в языке нет; пишут
func NewX(...) (*X, error)и делают поля неэкспортируемыми, чтобы нельзя было собрать невалидный объект литералом. Нулевое значение по возможности делают полезным (bytes.Buffer,sync.Mutexработают «из коробки»). - Чего нет. Наследования реализации, абстрактных классов, перегрузки методов, конструкторов/деструкторов, дженерик-методов, ковариантности. Многие GoF-паттерны либо не нужны, либо выражаются функцией высшего порядка (стратегия — просто
func, декоратор — обёртка, реализующая тот же интерфейс, какhttp.Handler-middleware).
Как добавление публичных геттеров и сеттеров к полям класса влияет на связность между модулями: она повысится или понизится?
Заголовок раздела «Как добавление публичных геттеров и сеттеров к полям класса влияет на связность между модулями: она повысится или понизится?»Коротко. Тривиальные публичные геттеры/сеттеры на каждое поле связанность (coupling) между модулями повышают, а не понижают: вы всё равно публикуете внутреннюю структуру данных, только через методы, и потребители начинают зависеть от неё. Единственное, что вы выигрываете по сравнению с публичными полями, — свободу поменять внутреннее представление, не ломая сигнатуры. При этом связность самого типа (cohesion) падает: он превращается в анемичный контейнер, а логика уезжает к вызывающим.
Глубже. Полезно сразу развести термины, потому что в русском их путают: cohesion («связность») — насколько элементы одного модуля работают на одну задачу, её хотят высокой; coupling («связанность», «зацепление») — насколько модули зависят друг от друга, её хотят низкой. Вопрос почти наверняка про coupling.
Разбор по существу: сила зависимости определяется не синтаксисом доступа, а тем, сколько знаний о вашем внутреннем устройстве требуется потребителю. Пара GetBalance()/SetBalance() сообщает ровно то же, что публичное поле Balance, и вдобавок легализует изменение состояния снаружи, минуя инварианты. Формально это «connascence of name» вместо «connascence of name + representation», то есть чуть слабее, но зависимость остаётся. Настоящее снижение связанности даёт замена аксессоров на поведение: вместо acc.SetBalance(acc.GetBalance() - sum) — acc.Withdraw(sum) error, который сам проверяет инварианты. Это принцип «Tell, Don’t Ask» и закон Деметры.
Специфика Go: геттеры пишут без префикса Get (u.Name(), а не u.GetName()) — так рекомендует Effective Go; сеттеры — SetName(). Массово генерировать аксессоры в Go не принято: если поле должно быть доступно на чтение и запись без инвариантов, его просто делают экспортируемым (User.Name), а метод заводят только когда нужна валидация, ленивое вычисление, конкурентная безопасность или скрытие представления (например, внутри time.Duration вместо двух полей). Отдельно стоит упомянуть DTO/wire-типы (сгенерированные protobuf-структуры с GetX()) — там аксессоры оправданы nil-безопасностью, а не инкапсуляцией.
Итоговая формулировка для собеседования: «Понизит только в одном узком смысле — модуль сможет менять внутреннее представление, не ломая компиляцию клиентов. По существу связанность останется прежней или вырастет: публичная поверхность станет больше, а инварианты — слабее. Снижает связанность не аксессор, а перенос поведения внутрь типа».
Как Go работает с IO-bound и CPU-bound задачами?
Заголовок раздела «Как Go работает с IO-bound и CPU-bound задачами?»Коротко. IO-bound — это сильная сторона: сетевые операции уходят в netpoller (epoll/kqueue/IOCP), «заблокированная» горутина снимается с потока, поэтому десятки тысяч одновременных запросов держатся на нескольких потоках ОС при синхронном стиле кода. CPU-bound Go тоже параллелит честно (GOMAXPROCS потоков считают одновременно, есть work stealing и асинхронное вытеснение), но абсолютная производительность вычислений ниже, чем у C++/Rust, и весь параллелизм ограничен числом ядер.
Глубже. Механика IO: conn.Read вызывает неблокирующий read; если данных нет, горутина паркуется (gopark), её g регистрируется в netpoller’е, а M берёт следующую работу. Когда epoll сообщает о готовности, горутина возвращается в очередь. Важное исключение — файловый IO: на Linux обычные файлы не работают с epoll, поэтому чтение файла — это настоящий блокирующий syscall, занимающий поток; при массовом файловом IO рантайм создаёт дополнительные потоки (лимит — 10000), и здесь имеет смысл ограничивать конкурентность семафором. То же касается вызовов через cgo и DNS-резолвера в cgo-режиме.
Механика CPU: параллельно исполняется не более GOMAXPROCS горутин. С Go 1.14 есть асинхронное вытеснение сигналом (SIGURG), поэтому длинный цикл без вызовов функций больше не блокирует GC и других горутин, но квант всё равно порядка 10 мс — для латентно-чувствительных задач крупные CPU-куски стоит нарезать самому. Практические правила: пул воркеров размером GOMAXPROCS (или чуть больше) для CPU-задач, а не «горутина на элемент»; батчинг, чтобы амортизировать издержки координации; никакого смысла в тысячах горутин на CPU-работе — они только увеличат переключения и память.
Смешанные нагрузки: разделяйте пулы — если CPU-задачи «съедят» все P, латентность IO-путей вырастет. В контейнерах обязательно приводите GOMAXPROCS к CPU-квоте cgroup (automaxprocs; в Go 1.25 рантайм умеет это сам), иначе на 64-ядерной ноде с лимитом в 1 CPU получите 64 P, лишние переключения и рваный throttling. И помните, что параллельность (GOMAXPROCS) и конкурентность (число горутин) — разные вещи: первая ограничена железом, вторая — только памятью.
Какие веб-фреймворки в Go вы использовали?
Заголовок раздела «Какие веб-фреймворки в Go вы использовали?»Коротко. Каркас ответа: в Go принято начинать с net/http из стандартной библиотеки и добирать роутер/middleware по необходимости — chi или gorilla/mux; из «настоящих» фреймворков наиболее распространены gin и echo, отдельно стоит fiber на fasthttp. С Go 1.22 в стандартный ServeMux завезли методы и wildcard-паттерны (mux.HandleFunc("GET /users/{id}", h)), после чего внешний роутер нужен заметно реже.
Глубже. Что стоит сказать по существу выбора:
net/http+chi.chiсовместим сhttp.Handler, то есть любая сторонняя middleware работает без адаптеров; даёт группы маршрутов, параметры пути,middleware.Recoverer, таймауты. Мой дефолт для сервисов: минимум магии, легко тестировать черезhttptest.gin/echo. СвойContext, биндинг и валидация из коробки, много готовой middleware. Цена — собственный тип обработчика (несовместимость с экосистемойhttp.Handler), больше «магии» в биндинге и меньше контроля над ошибками.fiber. Построен наfasthttp, а не наnet/http: другой API (в стиле Express), реальный выигрыш на очень простых эндпоинтах, но нет HTTP/2, нельзя использовать стандартные middleware и часть экосистемы, аfasthttpпереиспользует буферы — из-за чего легко получить use-after-free-подобные баги, если сохранить ссылку на тело/заголовки.- Что важнее фреймворка. Таймауты на
http.Server(ReadHeaderTimeout,WriteTimeout,IdleTimeout) — без них сервис уязвим к slowloris; middleware для recover, request id, логов и метрик; контекст запроса с дедлайном, прокинутый до БД; graceful shutdown;httptestв тестах вместо поднятия реального порта. - Смежное. Для gRPC —
grpc-go+protoc-gen-go-grpc, для REST поверх схемы —oapi-codegen/ogen(генерация из OpenAPI), для GraphQL —gqlgen.
Если опыта с фреймворками мало, честно скажите, с чем работали, и объясните критерии выбора — это ценится выше, чем список названий.
Является ли Go императивным или декларативным языком?
Заголовок раздела «Является ли Go императивным или декларативным языком?»Коротко. Императивным. См. выше подробный разбор: Go описывает последовательность изменений состояния, а декларативные элементы (теги структур, шаблоны, go:generate, схемы) в нём вторичны и приходят из инструментов, а не из ядра языка.
Глубже. Если вопрос задан второй раз, интервьюер обычно хочет либо уточнение классификации, либо примеры. Уточнение: Go — статически типизированный компилируемый императивный язык с процедурной основой, поддержкой объектно-ориентированного стиля через композицию и интерфейсы и с функциями как значениями первого класса (что позволяет писать в функциональном стиле, но не делает язык функциональным). Пример контраста в одну строку: SQL-запрос описывает результат, а for _, u := range users { if u.Age > 18 { adults = append(adults, u) } } описывает процедуру его получения.
Какая разница между мэйк и new?
Заголовок раздела «Какая разница между мэйк и new?»Коротко. См. выше: new(T) — выделяет обнулённую память под любой тип и возвращает *T; make — только для слайса, мапы и канала, инициализирует их внутренние структуры и возвращает значение самого типа. Практически: make — когда нужен рабочий слайс/мапа/канал, &T{} — во всех остальных случаях.
Частые ошибки на собесе
Заголовок раздела «Частые ошибки на собесе»- Говорят, что
makeвыделяет память в куче, аnew— на стеке. Оба builtin ничего не решают про стек и кучу; это делает escape-анализ компилятора. - Утверждают, что
newможно использовать для мапы или канала «какmake».new(map[K]V)даёт указатель наnil-мапу, запись в неё паникует. - Отвечают «в Go нет ООП» и на этом останавливаются. Правильный ответ — инкапсуляция, абстракция и полиморфизм есть, нет классов и наследования.
- Называют встраивание наследованием и обещают «переопределить метод». Виртуальности у структур нет: изнутри встроенного типа всегда вызовется его собственный метод.
- Считают, что чтение отсутствующего ключа из мапы паникует. Паникует только запись в
nil-мапу; чтение всегда безопасно и даёт нулевое значение. - Перечисляют «nil-абельные» типы неполно (забывают функции и интерфейсы) и не знают ловушку типизированного
nilв интерфейсе. - Путают уровни планирования: говорят, что горутина — это поток ОС или что планировщик Go управляет ядерными потоками. Планировщик пользовательский, модель M:N.
- Читают большой файл через
os.ReadFile/io.ReadAllи не помнят про лимит токенаbufio.Scannerв 64 КБ. - Изобретают имена функций
osпо «человеческой» логике (os.Cwd,os.SetPerms,os.ChangePermissions) вместо POSIX-имён (os.Getwd,os.Chmod,os.Chown,os.Symlink). - Считают линтер вопросом стиля. Основная ценность
go vet/staticcheck/errcheck— реальные дефекты, а не форматирование, которым занимаетсяgofmt. - Путать
makeиnewв деталях: говорить, что «newсоздаёт объект, аmake— примитив», или чтоmakeвозвращает указатель. Правильная формулировка — про инициализацию внутренних структур трёх встроенных типов и про возвращаемый тип. - Считать встраивание наследованием. Кандидаты уверенно говорят про «переопределение метода» — но динамической диспетчеризации к «наследнику» в Go нет: метод внешнего типа не подменяет вызов внутри метода встроенного типа.
- Утверждать, что порядок
init()между файлами гарантирован спецификацией. Он определён порядком передачи файлов компилятору (на практике алфавитным), а не языком; и уж точно нельзя строить на нём логику. - Ошибаться на iota: думать, что счётчик увеличивается на каждую константу (а не на каждую строку), и забывать, что он сбрасывается в каждом новом
const-блоке. - Говорить, что «интерфейс с nil-указателем внутри равен nil». Классика:
err != nil, хотя «ошибки не было». Нужно уметь объяснить это через пару (тип, значение). - Считать graceful shutdown синонимом
srv.Shutdown(). Забывают про readiness-пробу и задержку до начала остановки, проterminationGracePeriodSeconds, про hijacked-соединения и фоновые горутины, которыеShutdownне ждёт. - Оптимизировать без профиля. «Заменим на
sync.Pool, уберём интерфейсы, добавим горутин» — безpprof,benchstatи различенияinuse_space/alloc_space. - Рассказывать про рефлексию только на уровне «reflect.TypeOf возвращает тип», не упоминая цену, потерю проверок компилятора,
CanSetи требование указателя для изменения. - Отвечать на «минусы Go» либо «минусов нет», либо потоком жалоб. Ожидается взвешенный разбор компромиссов: за что заплачено и что получено взамен.
- Путать связность (cohesion) и связанность (coupling) в вопросе про геттеры/сеттеры и отвечать «понизится, потому что инкапсуляция».
Что почитать
Заголовок раздела «Что почитать»- Спецификация языка, разделы про builtin-функции и объявления: https://go.dev/ref/spec#Making_slices_maps_and_channels и https://go.dev/ref/spec#Declarations_and_scope
- Официальный FAQ, разделы «Is Go an object-oriented language?», «Why is there no type inheritance?»: https://go.dev/doc/faq
- Effective Go (композиция, встраивание, интерфейсы, идиомы): https://go.dev/doc/effective_go
- Документация пакета
osиio/fs: https://pkg.go.dev/os и https://pkg.go.dev/io/fs - Go Code Review Comments (в т.ч. про nil-слайсы и интерфейсы): https://go.dev/wiki/CodeReviewComments
- Исходники аллокатора и планировщика: https://github.com/golang/go/blob/master/src/runtime/malloc.go и https://github.com/golang/go/blob/master/src/runtime/proc.go
- Документация golangci-lint и staticcheck: https://golangci-lint.run/ и https://staticcheck.dev/docs/checks/
- Спецификация языка, разделы Package initialization, Constants (iota), Allocation/Making slices, maps and channels: https://go.dev/ref/spec
- Effective Go — идиомы, в том числе про геттеры, интерфейсы, встраивание и
init: https://go.dev/doc/effective_go - Release Notes Go 1.22 / 1.23 / 1.24 — сверять фичи по версиям: https://go.dev/doc/devel/release
- «The Laws of Reflection» (Rob Pike) и документация пакета
reflect: https://go.dev/blog/laws-of-reflection - Профилирование и диагностика: https://go.dev/doc/diagnostics , https://go.dev/blog/pprof и Profile-guided optimization: https://go.dev/doc/pgo
- Исходники рантайма про планировщик и cgo: https://github.com/golang/go/blob/master/src/runtime/proc.go , https://pkg.go.dev/cmd/cgo
Список исходных вопросов с привязкой к компаниям: ../questions/stdlib-tooling.md