---
title: "Гонки в тестах и конкурентный код: вопросы собеседования Go с ответами"
description: "Как тестировать конкурентный код Go: -race в CI, тестируемость через инъекцию синхронизации, goleak, тестирование каналов без sleep, таймауты — вопросы с ответами."
url: "https://golangmaster.ru/interview/gonki-v-testah/"
published: "2026-09-19"
updated: "2026-09-21"
level: "middle"
questions: 7
---

# Гонки в тестах и конкурентный код: вопросы собеседования Go с ответами

*Уровень: middle. ~19 минут, 7 вопросов. Источник: https://golangmaster.ru/interview/gonki-v-testah/*

«Как бы вы протестировали этот конкурентный код?» — вопрос, на котором сыпятся даже опытные: ответ «погоняю и посмотрю» не принимается. Ниже — приёмы, которые ждут: от тестируемости через инъекцию синхронизации до goleak и правильного использования -race. База конкурентности — в темах [горутины и каналы](/interview/gorutiny-i-kanaly/) и [паттерны каналов](/interview/patterny-kanalov/).

## Почему нельзя тестировать конкурентный код через time.Sleep?

Sleep делает тест одновременно медленным и ненадёжным: на медленной CI-машине сон мал — тест флакает красным, на быстрой — сон велик, тест минутами ждёт того, что происходит за миллисекунды. Правильная замена — синхронизация результата: канал, WaitGroup, sync.Cond. Если функция сигнализирует о завершении сама (канал результата, колбэк), тест читает сигнал; если нет — это дефект дизайна функции, и чинить надо её, а не тест. Формулировка для интервьюера: «sleep проверяет время, а тест должен проверять поведение».

```go
// ПЛОХО: time.Sleep(500 * time.Millisecond); assert(result)
// ХОРОШО:
select {
case res := <-resultCh:
    assert.Equal(t, want, res)
case <-time.After(2 * time.Second):
    t.Fatal("операция не завершилась за таймаут")
}
```

## Как сделать конкурентную функцию тестируемой? Инъекция синхронизации.

Приём: функция получает «канал завершения» или сигнальный интерфейс параметром, и тест подставляет детерминированную синхронизацию вместо реальной асинхронности. Варианты: колбэк onDone как параметр; метод принимает канал, в который пишет результат; зависимость-интерфейс (Clock/Scheduler) инъекцией — в тесте fake-часы, тикающие вручную. Идея одна: тест управляет моментами «когда», код — «что». Противопоказание: не добавлять параметры только ради теста, если поведение уже детерминировано — сначала убрать саму недетерминированность.

```go
// Функция тестируема: момент завершения управляется каналом
func worker(jobs <-chan Job, done chan<- struct{}) {
    for range jobs {}
    close(done)
}
// Тест: 1 задача → worker завершится детерминированно
jobs := make(chan Job); done := make(chan struct{})
go worker(jobs, done)
close(jobs)
<-done // детерминированное ожидание
```

## Как гонять тесты с детектором гонок в CI?

Флаг -race у go test (и go build -race для staging-бинарника): тесты компилируются с инструментацией обращений к памяти, детектор строит happens-before граф и роняет тест при гонке. Практики: -race в CI на каждый PR (цена — замедление ×2-20, память ×5-10 — приемлемо для тестов); гонять под -race тесты, реально выполняющие параллельные ветки (t.Parallel, конкурентные сценарии), иначе детектору нечего анализировать; флейки под -race — не «перезапустить», а чинить. Плюс: под -race паника от гонки даёт точные трассы обеих сторон — источник правды при разборе.

```go
go test -race -count=2 ./...
# count=2: два прогона ловят гонки, проявляющиеся не с первого раза
```

## Что такое goleak и как он ловит утечки горутин в тестах?

goleak (uber-go/goleak) — библиотека, которая сравнивает список живых горутин до и после теста: если после теста появились новые горутины, которых не было до — тест падает со стеком «виновника». Использование: defer goleak.VerifyNone(t) в начале теста (или TestMain для всех). Идеально ловит забытые воркеры, не закрытые подписки, зависшие на каналах горутины — то, что в проде было бы медленной утечкой. Нюанс: фоновые горутины самого приложения (пулы, поллеры) надо либо останавливать в тесте, либо исключить через goleak.IgnoreTopFunction.

```go
func TestWorkerStops(t *testing.T) {
    defer goleak.VerifyNone(t) // упадёт, если воркер не остановился
    stop := make(chan struct{})
    go worker(stop)
    close(stop)
}
```

## Как тестировать функцию, которая должна НЕ завершиться (бесконечный цикл обслуживания)?

Переворот вопроса: тестировать нужно не «не завершилась», а «реагирует на сигналы». Функцию обслуживания делают управляемой (done-канал, контекст, инъекция), и тест проверяет: (1) по сигналу она завершается — детерминированно через <-done с таймаутом; (2) во время работы обрабатывает входы — через каналы теста; (3) после завершения не оставляет горутин — goleak. Прямой тест «бесконечный цикл не завершился» невозможен по определению — это маркер, что функцию надо рефакторить в управляемую форму. На собеседовании этот переворот — ключевой ответ.

```go
func TestServiceStopsOnSignal(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    done := make(chan struct{})
    go func() { svc.Run(ctx); close(done) }()
    cancel()
    select {
    case <-done: // штатная остановка
    case <-time.After(2 * time.Second):
        t.Fatal("сервис не остановился по отмене контекста")
    }
}
```

## Почему -count=2 и -shuffle находят больше багов?

-count=2 (и больше) прогоняет каждый тест несколько раз: ловит зависимость от порядка, неочищенное состояние между прогонами и гонки, проявляющиеся не с первого раза. -shuffle=on перемешивает порядок тестов: тесты, падающие при другом порядке, зависят от глобального состояния (пакетные переменные, кэш) — зависимость от порядка это дефект. Тесты должны быть изолированы: любой тест обязан проходить в одиночку, в любом порядке и много раз подряд. В CI: go test -race -count=2 -shuffle=on ./... — минимальный набор, ловящий целый класс «работает на моей машине».

```go
go test -race -count=2 -shuffle=on ./...
```

## Как тестировать, что операция действительно конкурентна (а не последовательна под капотом)?

Приём «барьер синхронизации»: запустить N операций параллельно (WaitGroup), и внутри каждой блокироваться на общем барьер-канале, который тест открывает после того, как все дошли до него. Если реализация «параллельна» лишь формально (последовательный цикл под капотом), тест зависнет на барьере и упадёт по таймауту. Альтернатива для API-уровня: замер времени — N операций за время, сопоставимое с одной (с запасом), но временные тесты флакают, поэтому барьер — основной приём, время — вспомогательная метрика.

```go
barrier := make(chan struct{})
var wg sync.WaitGroup
results := make([]Result, 5)
for i := range results {
    wg.Add(1)
    go func(i int) {
        defer wg.Done()
        <-barrier // все дошли — значит, работали параллельно
        results[i] = fetch(i)
    }(i)
}
close(barrier) // открыли барьер
wg.Wait()
```
