Worker pool на Go: кто закрывает канал и как дождаться обработки

Задачи отправлены в канал, main завершился, и часть работы исчезла. Либо несколько работников закрывают один канал и получают panic. Для корректного завершения нужно разделить владельца входного канала и ожидание исполнителей.

Как разобрать ситуацию

В простом пуле производитель закрывает канал после последней отправки, а workers читают его через range. WaitGroup позволяет дождаться их окончания. Закрытие канала не означает, что вся обработка уже завершилась: сообщения могли быть прочитаны, но работа ещё продолжается. Это отдельное состояние жизненного цикла.

Пример и ожидаемый результат

Два работника суммируют задания 1, 2 и 3. Вход закрывается единственным производителем; общая сумма защищена mutex. После Wait программа должна вывести 6 независимо от порядка распределения задач между работниками.

package main

import (
	"fmt"
	"sync"
)

func main() {
	jobs := make(chan int)
	var wg sync.WaitGroup
	var mu sync.Mutex
	sum := 0
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for x := range jobs {
				mu.Lock()
				sum += x
				mu.Unlock()
			}
		}()
	}
	for _, x := range []int{1, 2, 3} {
		jobs <- x
	}
	close(jobs)
	wg.Wait()
	fmt.Println(sum)
}

Ожидаемый вывод — 6. Пример подтверждает завершение всех локальных обработчиков и синхронизацию общей суммы, но не является надёжной распределённой очередью.

Проверка перед применением

  • Проверьте пустой набор и один элемент.
  • Убедитесь, что только владелец отправки решает, когда новых задач больше не будет.
  • Для отмены определите судьбу уже принятых, но не завершённых заданий: завершить, вернуть в очередь или пометить ошибкой.

Для рабочего пула добавьте метрики: сколько задач принято, выполняется, завершено и отклонено. Если вы видите только число отправленных сообщений, потеря при остановке останется невидимой. Небольшой тест завершения полезнее увеличения буфера канала: буфер меняет вместимость, но не создаёт гарантию доставки.

Границы решения

Канал находится в памяти процесса: при аварии его содержимое теряется. Для важных фоновых операций нужны внешнее хранилище, подтверждения и повторная обработка. Программа выполнена обычным go run; отдельный race detector этим запуском не включается.

Если базовый Go уже знаком и хочется связать HTTP, хранение данных и обработку отказов в одну систему, посмотрите наш платный курс

Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker. Перед записью сравните программу со своей задачей и подготовкой.

Техническая справка: go.dev: документация, pkg.go.dev: документация.

Следующий шаг: Go backend: HTTP, конкурентность и устойчивость сервиса. По смежным вопросам: Как оборачивать ошибки в Go и не потерять машинную проверку причины, Плюс и амперсанд потерялись в URL Go-клиента: как собрать query.