Yesterday

Go (Golang). Средний уровень.

Дальше — про практику сервиса. Тест проверяет, как вы работаете с памятью и конкурентностью в реальных задачах. Типичные темы: указатели и приёмники методов, влияние срезов на массив, буферизация каналов и дедлоки, роутинг net/http, пул воркеров, ограничения httptest, проверка Origin в WebSocket, рефлексия, контекст, строки, gRPC-паттерны.


Вопрос 1. Какой из следующих вариантов корректно выделяет код в функцию для увеличения значения поля структуры на заданное число?

Варианты ответов
A) func increment(n int, c Counter) {}
B) func increment(c Counter, n int) {}
C) func (c Counter) increment(n int) {}
D) func (c *Counter) increment(n int) {}
E) func increment(c *Counter, n int) {}

В main вызов — increment(&c, 5), значит функция принимает указатель на Counter и число. Так мы меняем исходную структуру, а не копию. Сигнатура должна быть функцией (не методом) и первым параметром — *Counter.

Варианты C/D — это методы, их бы вызывали c.increment(5). Варианты A/B берут Counter по значению — изменения не сохранятся. Остаётся единственный подходящий — функция с *Counter.

Выбранныйответ: E) func increment(c *Counter, n int) {}


Вопрос 2 Какой будет результат выполнения следующей программы?

Варианты ответов
A) [1 10 3]
B) [1 2 3]
C) [1 2 10]
D) [10 10 3]
E) [10 2 3]

arr — массив [1 2 3]. s := arr[1:] — срез, указывающий на тот же базовый массив, начиная с элемента с индексом 1 ([2 3]). Изменение s[0] = 10 меняет arr[1] на 10. Печатаем исходный массив — получаем [1 10 3].

Ключ — помнить, что срезы не копируют данные, а «смотрят» в исходный массив. Изменения через срез отражаются в массиве. Поэтому меняется только средний элемент.

Выбранный ответ: A) [1 10 3]


Вопрос 3. Вы разрабатываете библиотеку коллекций. Нужно спроектировать интерфейс и структуры стека и очереди, работающие с элементами любого типа. Какой подход наиболее правильный?

Варианты ответов
A) Использовать дженерики (generics) для создания типобезопасных реализаций стека и очереди
B) Создать отдельные реализации для каждого типа (IntStack, StringStack, …)
C) Создать интерфейс Collection с методами Add()/Remove(), который будут реализовывать Stack и Queue
D) Использовать пустой интерфейс interface{} как тип элементов
E) Использовать встроенные типы (слайсы/мапы) без отдельных структур и интерфейсов

Generics в Go позволяют параметризовать тип элемента: Stack[T any], Queue[T any]. Это даёт и универсальность, и типобезопасность без приведения типов и без копипаста реализаций. Вариант с interface{} теряет типобезопасность и требует приведения/проверок во время выполнения. Отдельные реализации для каждого типа — дублирование кода. Обычный интерфейс без параметров не решает типизацию элементов. Использование «просто слайсов» — не библиотека и не скрывает инварианты структуры данных.

Цель — «любой тип» плюс удобный API и безопасность типов. После Go 1.18 единственно зрелое решение — дженерики. Всё остальное — либо боль с приведениями, либо размножение однотипного кода.

Выбранный ответ: A) Использовать дженерики (generics) для создания типобезопасных реализаций стека и очереди


Вопрос 4. Вам нужно реализовать сервер, который маршрутизирует по разным путям (/users, /orders). Какой инструмент пакета net/http корректен для этой задачи?

Варианты ответов
A) Использовать http.HandleFunc для регистрации разных обработчиков по разным путям
B) Использовать http.ListenAndServe без регистрации обработчиков
C) Использовать http.ServeFile для всех путей
D) Использовать только один обработчик и вручную разбирать путь из r.URL.Path
E) Использовать http.ServeMux без регистрации обработчиков, ожидая, что маршруты создадутся автоматически

В стандартной библиотеке маршрутизация решается регистрацией обработчиков на пути. http.HandleFunc(pattern, handler) регистрирует функцию-обработчик на DefaultServeMux (или вы можете создать свой ServeMux и вызывать mux.HandleFunc). Это ровно то, что нужно для разных маршрутов. ListenAndServe только запускает сервер. ServeFile — для выдачи файлов. Один общий обработчик с ручным парсингом пути — костыль. ServeMux без регистрации ничего не сделает.

Когда нужны простые маршруты — не тянуть сторонний роутер, достаточно HandleFunc/ServeMux. Это читаемо и прозрачно: по коду сразу видно, какой путь к какому хендлеру привязан.

Выбранный ответ: A) Использовать http.HandleFunc для регистрации разных обработчиков по разным путям


Вопрос 5. У вас есть несколько горутин, которые одновременно записывают данные в общую структуру. Какой примитив синхронизации выбрать, чтобы предотвратить гонку данных?

Варианты ответов
A) sync.WaitGroup
B) sync.Pool
C) sync.Once
D) sync.Mutex
E) sync.Cond

Для защиты общего ресурса нужен примитив взаимного исключения — чтобы только одна горутина в момент времени выполняла критическую секцию записи. В Go для этого предназначен sync.Mutex (или специализированный sync.RWMutex для разделения чтения/записи).
WaitGroup лишь ждёт завершения горутин, гонки не предотвращает. Pool — кэш объектов. Once выполняет код один раз. Cond — механизм оповещения/ожидания, но сам по себе данные не защищает.

Смотрю на формулировку «одновременно записывают» — ключевое слово «взаимное исключение». Из перечисленного прямой ответственный за это только мьютекс; всё остальное — про учёт, кэширование или сигнализацию.

Выбранный ответ: D) sync.Mutex


Вопрос 6. Почему программа с ch := make(chan int); ch <- 1 «зависает» и не завершается?

Варианты ответов
A) Проблема в том, что значение не было считано из канала, поэтому оно теряется
B) Проблема в том, что отправка в небуферизованный канал блокируется, если нет получателя
C) Проблема в том, что канал был объявлен как односторонний
D) Проблема в том, что канал был закрыт до отправки значения
E) Проблема в том, что канал был объявлен как буферизованный, но буфер заполнен

Канал создан без буфера (make(chan int)). Отправка в такой канал блокируется, пока другая горутина не выполнит чтение. Получателя нет — главная горутина залипает на ch <- 1, до Println не доходит. Решения: добавить буфер make(chan int, 1) или организовать чтение/запись в разных горутинах.

Код без go и без чтения — классический deadlock на небуферизованном канале. Если бы канал был буферизован, отправка одного значения прошла бы; если бы он был закрыт — паника, а не «висение».

Выбранный ответ: B) Проблема в том, что отправка в небуферизованный канал блокируется, если нет получателя


Вопрос 7. Есть ли в фрагменте кода гонка данных и каким примитивом синхронизации её устранить?

Варианты ответов
A) Гонка данных присутствует, устранить с помощью sync.Mutex
B) Гонка данных присутствует, устранить с помощью sync.Once
C) Гонка данных присутствует, устранить с помощью sync.Pool
D) Гонка данных отсутствует, код корректен
E) Гонка данных присутствует, устранить с помощью sync.Cond

Несколько горутин одновременно выполняют counter++. Это две операции (чтение → инкремент → запись), которые без защиты могут переписать друг друга — типичная гонка. WaitGroup только ждёт завершения, но не защищает доступ. Для безопасной записи нужен мьютекс: mu.Lock(); counter++; mu.Unlock() (или атомики, но их нет в вариантах).

Смотрю на признак гонки: общий изменяемый счётчик плюс параллельные инкременты. Любые ответы про Once/Poll/Cond не решают взаимоисключение. Остаётся мьютекс как прямой и корректный способ.

Выбранный ответ: A) Гонка данных присутствует, устранить с помощью sync.Mutex


Вопрос 7. Нужно протестировать GetUser без использования реальной базы данных. Какой подход выбрать?

Варианты ответов
A) Добавить в GetUser условную логику, определяющую тестовый режим
B) Использовать глобальные переменные для тестовых данных и переключения режимов
C) Изменить GetUser, добавив параметр «тестовый режим»
D) Создать временную БД для тестов и использовать её вместо основной
E) Создать реализацию интерфейса UserRepository, возвращающую предопределённые данные для тестов (мок)

В коде репозиторий вынесен в интерфейс UserRepository и передаётся в UserService через конструктор. Это специально сделано для подмены зависимостей. В тесте создаём мок-реализацию UserRepository (или стаб), который возвращает фиксированные данные, и передаём его в NewUserService. Так мы изолируем логику сервиса от БД и получаем быстрые, детерминированные unit-тесты.

Флаги «тестовый режим» и глобальные переменные усложняют код и ломают изоляцию. Временная БД — это уже интеграционные тесты: медленно и не то, что спрашивают («без реальной БД»). Интерфейс+мок — каноничный путь для юнит-тестов в Go.

Выбранный ответ: E) Создать реализацию интерфейса UserRepository, возвращающую предопределённые данные для тестов (мок)


Вопрос 8. Что наиболее точно описывает рефлексию?

Варианты ответов
A) Рефлексия — это инструмент отладки, выводящий доп. информацию
B) Рефлексия — это параметр профилирования через goprof
C) Рефлексия — это техника для создания UI через отображение поведения объекта
D) Рефлексия — это метод оптимизации, изменяющий внутреннее устройство объекта
E) Рефлексия — это механизм, позволяющий программе анализировать и изменять свою структуру и поведение во время выполнения

Рефлексия в Go — это пакет reflect, который позволяет в рантайме смотреть на тип и значение (type/value), а также, где это допускается, изменять значения через отражение. Это не про отладку, не про профилирование и не про «ускорение кода».

Ищу формулировку про «исследовать/менять во время выполнения» — это сущность reflection. Остальные ответы путают с логированием, профайлингом или оптимизациями.

Выбранный ответ: E) Рефлексия — это механизм, позволяющий программе анализировать и изменять свою структуру и поведение во время выполнения


Вопрос 9. Какое из следующих утверждений о gRPC является неверным?

Варианты ответов
A) gRPC позволяет сгенерировать серверный и клиентский код на основе файлов .proto
B) gRPC позволяет определять сервисы и методы в файлах с расширением .proto
C) gRPC поддерживает двунаправленную потоковую передачу данных
D) gRPC использует Protocol Buffers (protobuf) для сериализации данных
E) gRPC поддерживает только синхронные вызовы

gRPC по умолчанию работает поверх HTTP/2 и поддерживает четыре вида вызовов: unary, server-streaming, client-streaming и bidirectional streaming. Это означает наличие асинхронного взаимодействия; ограничение “только синхронные вызовы” неверно. Остальные пункты описывают стандартный рабочий процесс: описываем сервисы/сообщения в .proto и генерируем код, обычно с protobuf.

Ключевой маркер — «только синхронные». gRPC славится потоками и стримингом, значит утверждение противоречит сути технологии. Остальное — привычные факты про .proto и генерацию.

Выбранный ответ: E) gRPC поддерживает только синхронные вызовы


Вопрос 10. Какой из следующих вариантов наиболее корректно описывает назначение контекста (context)?

Варианты ответов
A) Контекст применяется только для тестирования
B) Контекст необходим для сериализации данных в JSON
C) Контекст предназначен для хранения пользовательских данных между функциями
D) Контекст используется для передачи сигналов отмены, и метаданных между горутинами
E) Контекст используется только для логирования ошибок

context.Context несёт дедлайны/таймауты, сигнал отмены и request-scoped значения через границы вызовов и между горутинами. Это помогает корректно завершать операции и пробрасывать техметаданные (trace-id, locale и т. п.). Он не про JSON, не только про логи и не «контейнер для пользовательских данных».

Ищу формулировку, где есть «отмена/дедлайн + значения». Только вариант D покрывает этот смысл. Пункт C опасен: контекст не должен хранить бизнес-данные — только небольшие метаданные.

Выбранный ответ: D) Контекст используется для передачи сигналов отмены, и метаданных между горутинами


Вопрос 12. Какое из следующих утверждений о строках является верным?

Варианты ответов
A) Строка — это срез рун
B) Строка всегда состоит из символов Unicode
C) Строка представляет собой неизменяемую последовательность байт
D) Строка может быть изменена напрямую
E) Строка всегда заканчивается нулевым байтом

В Go строка — это отдельный тип, хранящий неизменяемую последовательность байт. Обычно это UTF-8, но технически в строке могут быть любые байты (в т. ч. невалидный UTF-8). Поэтому строка — не срез рун, менять её «на месте» нельзя, нулевым байтом она не завершается.

Проверяю признаки: «неизменяемость» и «байты» — ключ к строкам в Go. Любые формулировки про «срез рун», «всегда Unicode» или «нулевой байт» — это про другие языки/модели.

Выбранный ответ: C) Строка представляет собой неизменяемую последовательность байт

Заключение
Средний уровень — умение видеть последствия. Где нужен мьютекс, где — канал; как правильно прокинуть context; почему «зелёный» тест не означает стабильный прод. Вы принимаете решения, исходя из модели рантайма и поведения библиотек, а не из привычки. Проектируете безопасный и предсказуемый код, учитываете среду выполнения.