Задание 1: табличный тест для параллельной SumSquares
Первое задание практики про качество кода — не новая бизнес-логика, а табличный тест на уже готовую параллельную функцию SumSquares. Тест был расставлен как есть с двумя TODO: пустой слайс кейсов и незаполненная проверка результата.
Что делает SumSquares
func SumSquares(numbers []int) (int, error) {
results := make(chan int)
errorsCh := make(chan error, 1)
var workers sync.WaitGroup
for _, number := range numbers {
workers.Add(1)
go func() {
defer workers.Done()
if number < 0 {
select {
case errorsCh <- fmt.Errorf("%w: %d", ErrNegativeNumber, number):
default:
}
return
}
results <- number * number
}()
}
go func() {
workers.Wait()
close(results)
}()
total := 0
for result := range results {
total += result
}
select {
case err := <-errorsCh:
return 0, err
default:
return total, nil
}
}На каждое число запускается отдельная горутина: неотрицательные считают квадрат и отправляют его в results, отрицательные пишут обёрнутую ошибку в errorsCh и завершаются без записи в results. Отдельная горутина ждёт workers.Wait() и закрывает results — это стандартный приём, чтобы for result := range results в основном потоке корректно завершился, когда все воркеры отработали, а не висел вечно.
Форма табличного теста
Табличный тест — слайс анонимных структур, где каждый элемент описывает один сценарий: вход, ожидаемый результат, ожидаемая ошибка. Цикл прогоняет их все через t.Run, что даёт отдельную строку в выводе go test -v на каждый кейс и позволяет сфокусированно перезапустить один из них через -run.
tests := []struct {
name string
numbers []int
want int
wantErr error
}{
{
name: "обычный набор чисел",
numbers: []int{1, 2, 3},
want: 14,
wantErr: nil,
},
{
name: "пустой слайс",
numbers: []int{},
want: 0,
wantErr: nil,
},
{
name: "отрицательное число",
numbers: []int{1, -2, 3},
want: 0,
wantErr: ErrNegativeNumber,
},
}1² + 2² + 3² = 1 + 4 + 9 = 14 — обычный случай без ошибок. Пустой слайс — краевой случай: цикл по numbers не запускает ни одной горутины, workers.Wait() возвращается мгновенно, results закрывается пустым, total остаётся 0. Третий случай — то же самое подмножество чисел, но с одним отрицательным: ожидаемый результат не важен (функция гарантированно вернёт 0 при ошибке), важна сама ошибка.
errors.Is, а не прямое сравнение
Функция оборачивает ошибку через %w, а не возвращает ErrNegativeNumber напрямую:
fmt.Errorf("%w: %d", ErrNegativeNumber, number)Из-за этого сравнение err == ErrNegativeNumber всегда будет false — err это новое значение типа *fmt.wrapError, а не тот же указатель, что ErrNegativeNumber. errors.Is разворачивает цепочку через метод Unwrap(), который автоматически генерируется для %w, и находит ErrNegativeNumber внутри — поэтому именно он, а не ==, должен стоять в проверке теста:
if tt.wantErr != nil {
if !errors.Is(err, tt.wantErr) {
t.Fatalf("SumSquares() error = %v, want wrapping %v", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("SumSquares() unexpected error = %v", err)
}
if got != tt.want {
t.Errorf("SumSquares() = %d, want %d", got, tt.want)
}t.Fatalf на неожиданной ошибке останавливает подтест сразу — дальше сравнивать got бессмысленно, значение всё равно 0. t.Errorf в последней проверке — обычная практика: несовпадение результата не мешает тесту продолжить (здесь дальше и так нечего проверять, но это привычка, которая окупается в более длинных тестах, где после несовпадения одного поля стоит проверить остальные).
Одна ошибка на несколько отрицательных чисел — не баг, а особенность буфера
errorsCh := make(chan error, 1) — буфер ровно на одно значение. Если в списке несколько отрицательных чисел, каждая соответствующая горутина пытается отправить свою ошибку через select с default: первая, кто успеет, займёт единственное место в буфере, все остальные попадут в ветку default и молча отбросят свою ошибку.
Это не влияет на тест из задания — там ровно одно отрицательное число, поведение детерминированное. Но стоит держать в голове при чтении кода: SumSquares([]int{-1, -2, -3}) гарантированно вернёт ошибку про какое-то отрицательное число, но не гарантирует, про какое именно — это состояние гонки между тремя горутинами за один слот буфера. Для функции, где важно поймать именно первую или именно все ошибки, такая конструкция не подошла бы; здесь она осознанно упрощена, потому что вызывающему коду достаточно знать сам факт «был отрицательный вход».
Запустить и проверить
go test -v ./01-table-test
Ожидаемый вывод — три PASS подтеста под общим TestSumSquares, каждый по имени сценария (кириллица в имени t.Run автоматически подменяется движком go test на подчёркивания в выводе — это нормально, не ошибка).
Живой пример работы функции — тот же код SumSquares, что в задании, прогнанный на успешном и ошибочном входе: