Yesterday

Go (Golang): Базовый уровень.

Начинаем с фундамента. Задачи простые по форме, но требуют понимания моделей Go. Зачем проходить тест? Чтобы проверить, как вы читаете код и предсказываете поведение без запуска. Часто встречаются вопросы про константы и типы, срезы и карты, цикл for … range, интерфейсы, указатели, минимальный HTTP-сервер, горутины и базовые тесты.


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

Варианты ответов
A) const y = 5
B) const y int = 5
C) const header = []byte{1,2,3}
D) const y float64 = 3.14
E) const y = "hello"

В Go константами могут быть только значения, известные на этапе компиляции: числа, строки, руны, булевы. Срезы, мапы, функции и т. п. — это НЕ константы. Литерал []byte{1,2,3} создаёт срез (runtime-значение), поэтому с const он недопустим. Остальные варианты — корректные числовые/строковые константы (типизированные и нет).

Критерий простой: «компиляторное значение или нет». Срез — всегда динамическая структура, значит отпадает. Числа/строки — ок, даже с явным типом.

Выбранный ответ: C) constheader = []byte{1,2,3}


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

Варианты ответов
A) Указатели используются для работы с сетевыми соединениями и предоставляют удобный интерфейс для обработки HTTP-запросов
B) Указатели автоматически создаются для каждой переменной, и разработчик не управляет ими вручную
C) Указатели — это ключевое слово, определяющее область видимости переменной в программе
D) Указатели позволяют напрямую изменять значение переменной, переданной в функцию, без её копирования
E) Указатели в Go используются только для взаимодействия с внешними библиотеками на C через cgo

Указатель — это значение, хранящее адрес другой переменной. Передавая в функцию указатель (а не копию значения), можно менять исходные данные и избегать лишнего копирования. Остальные формулировки неверны: указатели не «для HTTP», не создаются автоматически для каждой переменной, не связаны с областью видимости и, разумеется, используются далеко не только в cgo.

Ключевой сигнал — слова «изменять значение… без копирования». Это базовый мотив использования указателей в Go. Остальные варианты либо путают области (сеть, cgo), либо описывают механики, которых в языке нет (автосоздание, «ключевое слово» про видимость).

Выбранный ответ: D) Указатели позволяют напрямую изменять значение переменной, переданной в функцию, без её копирования


Вопрос 3. Какой из следующих вариантов корректно реализует функцию для перебора элементов среза?

Варианты ответов
A) index, fruit in range
B) for index, fruit in range fruits
C) index, fruit := range fruits
D) fruits.Each(index, fruit)
E) for index, fruit := range fruits

В Go перебор среза делается через for ... range. Нужны три вещи: ключевое слово for, операция := для объявления переменных индекса и значения, и объект после range. Итоговый шаблон:
for index, fruit := range fruits { ... }
Варианты с in — это синтаксис не из Go; без for — тоже ошибка; вызов Each — не часть стандартного языка.

Смотрю на ключевые маркеры Go: for + range + :=. Только вариант с ними тремя совпадает с тем, как я пишу циклы в коде ежедневно. Остальные — явные «замены» из других языков или неполные конструкции.

Выбранный ответ: E) forindex, fruit := rangefruits


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

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

map[string]struct{} даёт амортизированное O(n) по времени: один проход, проверка наличия — O(1).
Сортировка — O(n log n) и ломает исходный порядок.
Линейная проверка наличия в новом срезе — O(n²).
Массив фиксированной длины неприменим (не знаем размер и поиск линейный).
Рекурсия здесь не даёт выигрыша и усложняет код.

Критерий — минимизировать асимптотику. В проде обычно делаю seen := map[string]struct{} и добавляю в результирующий срез только новые элементы — быстро и сохраняет порядок появления. Сортировку беру лишь если порядок не важен и нужно постобработать.

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


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

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

В Go интерфейс — это контракт: набор методов. Любой тип, у которого есть эти методы, неявно удовлетворяет интерфейсу. Это основа полиморфизма в Go (пример: io.Reader, fmt.Stringer). Остальные варианты неверны: интерфейсы как раз и передают поведение, их реализуют не только структуры, они могут хранить значения любых типов, и их применение далеко не ограничено внешними библиотеками.

Ключевая идея Go — “согласие по методу, а не по объявлению”. Я смотрю, где описан именно «набор методов». Остальные ответы либо путают назначение (про поведение), либо приписывают интерфейсам ограничения, которых нет.

Выбранный ответ: A) Интерфейсы позволяют определять набор методов, которые должны реализовать типы


Вопрос 6. Какое из утверждений о срезах (slices) верное?

Варианты ответов
A) Функция append() никогда не изменяет емкость (capacity) исходного среза
B) Срезы в Go имеют фиксированную длину, которая не может быть изменена после создания
C) Срезы в Go могут содержать элементы разных типов данных
D) При передаче среза в функцию всегда создаётся его полная копия с копированием всех элементов
E) Срезы в Go являются ссылочным типом данных и указывают на базовый массив

Срез — это «описатель» поверх массива: он хранит указатель на базовый массив, длину и ёмкость. Поэтому изменения через срез обычно затрагивают тот же массив. append возвращает новый срез; при нехватке capacity он аллоцирует новый массив — значит утверждение «никогда не изменяет» неверно. Длина среза не фиксирована (меняется через append/реслайсинг). Срезы типизированы — элементы одного типа. Передача среза в функцию копирует только описатель, а не все элементы.

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

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


Вопрос 7. Какой из следующих шагов НЕ является обязательным при создании и запуске HTTP-сервера?

Варианты ответов
A) Создать функцию-обработчик, которая принимает параметры http.ResponseWriter и *http.Request
B) Зарегистрировать функцию-обработчик с помощью http.HandleFunc
C) Запустить сервер с помощью http.ListenAndServe
D) Создать структуру для хранения состояния сервера и передать её в функцию-обработчик
E) Импортировать пакет net/http

Базовый сервер в Go требует: импорт net/http, наличие обработчика (функции или типа с ServeHTTP), регистрацию маршрута (через HandleFunc/Handle или передачу собственного mux) и запуск (ListenAndServe или эквивалент). Отдельная структура состояния — это удобный паттерн, но не обязательный шаг.

Смотрю, что является «ритуальными» минимальными действиями для hello-world сервера. Структура для состояния — это про дизайн и тестируемость; сервер запустится и без неё.

Выбранный ответ: D) Создать структуру для хранения состояния сервера и передать её в функцию-обработчик


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

Варианты ответов
A) Горутины могут взаимодействовать друг с другом с помощью каналов
B) Горутины — это легковесные потоки, которые управляются рантаймом Go
C) Горутины могут выполняться параллельно на нескольких ядрах, если программа запущена на многоядерной системе
D) Горутины всегда завершаются в том порядке, в котором они были запущены
E) Горутины создаются с помощью ключевого слова go, за которым следует вызов функции

Рантайм Go планирует горутины независимо; порядок их завершения не гарантируется. Каналы — штатный механизм взаимодействия. Параллелизм возможен на многоядерной системе (см. GOMAXPROCS). Создание горутины — оператор go f().

Вопрос про свойства планировщика: если бы завершение шло строго по порядку запуска, конкуренции как таковой не было бы. На практике горутины стартуют и заканчиваются в произвольном порядке — это базовый принцип.

Выбранный ответ: D) Горутины всегда завершаются в том порядке, в котором они были запущены


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

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

sync.Mutex — примитив взаимного исключения: Lock() блокирует, Unlock() разблокирует. Он нужен, чтобы защищать общий ресурс от одновременной записи/чтения несколькими горутинами. Он не имеет отношения к каналам, не завершает горутины и не «срабатывает сам по себе» без явных вызовов.

Ключ — что делает мьютекс: лишь «один заходит — остальные ждут». Варианты про каналы и автозавершение — мимо. Про main — искусственное ограничение; мьютекс — обычный тип, где угодно.

Выбранный ответ: E) sync.Mutex обеспечивает взаимное исключение при доступе к разделяемым данным


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

Варианты ответов
A) Тестовые функции могут иметь произвольное имя
B) Тестовые функции должны начинаться с префикса Test
C) Тестовые функции должны возвращать значение типа bool
D) Тестовые функции не могут использовать методы t.Error и t.Fail
E) Тестовые функции должны быть объявлены в пакете main

В Go тесты — это функции вида func TestXxx(t *testing.T) в файлах *_test.go. Имя обязательно начинается с Test, возвращаемых значений нет, а для фиксации ошибок как раз используются методы t.Error, t.Fail, t.Fatalf и т. п. Тесты обычно пишут в том же пакете, что и код (или в package name_test), но не обязаны быть в main.

Ищу «ритуальное» правило распознавания тестов раннером go test. Это именно префикс Test и сигнатура с *testing.T. Остальные варианты либо добавляют несуществующие требования (возвращать bool, пакет main), либо прямо противоречат практике (про произвольное имя и запрет t.Error/t.Fail).

Выбранный ответ: B) Тестовые функции должны начинаться с префикса Test

Заключение
Базовый уровень — это не про синтаксис, а про ментальные опоры: «срез — окно в массив», «интерфейс — контракт по методам», «канал — обмен и синхронизация», «константа — значение компилятора». Если это щёлкает в голове, вы пишете предсказуемый код и меньше дебажите. Понимаете основные абстракции Go и не наступаете на простые грабли.