Мастерство конкурентности в Go:
Итак вторая часть продолжаем. Теперь давайте посмотрим, как эти примитивы объединяются, образуя мощные шаблоны, которые решают реальные проблемы.
В этой статье мы рассмотрим генератор и попытаемся визуализировать его. Итак, давайте подготовимся, поскольку весь процесс пройдем с примерами.
Генератор
Генератор как фонтан, который непрерывно производит значения, которые мы можем использовать при необходимости.
В Go это функция, которая создает поток значений и отправляет их по каналу, позволяя другим частям нашей программы получать эти значения по запросу.

К примеру:
// generateNumbers создает генератор, который выдает числа от 1 до max.
func generateNumbers(max int) chan int {
// Создаем канал для отправки сообщений
out := make(chan int)
// Запускаем горутину для генерации чисел
go func() {
// ВАЖНО: всегда закрываем канал после завершения
defer close(out)
for i := 1; i <= max; i++ {
out <- i // Кладем значение в канал
}
}()
// Возвращаем канал
return out
}
// Используем генератор
func main() {
// Создаем генератор который генерит числа 1-5
numbers := generateNumbers(5)
// Получаем числа
for num := range numbers {
fmt.Println("Received:", num)
}
}
В этом примере наша функция генератора выполняет три ключевые задачи:
Создает канал для отправки значений
Запускает горутину для генерации значений
Немедленно возвращ��ет канал для использования потребителями
Зачем использовать генераторы?
Отделите создание значений от потребления
Генерируйте значения по требованию (lazy evaluation)
Позволяют представлять бесконечные последовательности без использования бесконечной памяти
Обеспечивают конкурентное производство и потребление значений
Пример
Чтение большого файла построчно.
func generateLines(filename string) chan string {
out := make(chan string)
go func() {
defer close(out)
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
out <- scanner.Text()
}
}()
return out
}
Теперь вы, возможно, думаете: что в этом особенного? Мы можем делать то же самое, например, генерировать последовательность данных или считывать строки по одной без горутин. Не является ли это избыточным? Давайте попробуем визуализировать оба случая:
Без горутин
func getNumbers(max int) []int {
numbers := make([]int, max)
for i := 1; i <= max; i++ {
numbers[i-1] = i
// Тяжелые вычисления тут
time.Sleep(100 * time.Millisecond)
}
return numbers
}
В этом случае вам придется ждать пока не закончатся все вычисления прежде чем получить какой-то результат. Все или ничего.
Вызываем горутину
func generateNumbers(max int) chan int {
out := make(chan int)
go func() {
defer close(out)
for i := 1; i <= max; i++ {
out <- i
// Тяжелые вычисления тут
time.Sleep(100 * time.Millisecond)
}
}()
return out
}
Разница в том что, во втором случае мы можем обрабатывать данные сразу, а в первом случае мы должны ждать пока функция не отработает полностью(поставьте таймер на 1000 * time.Millisecond)

Ключевые преимущества паттерна «Генератор» (Generator Pattern):
Неблокирующее выполнение — Генерация и обработка происходят одновременно, что позволяет эффективно использовать ресурсы и не ждать полной загрузки данных.
Эффективное использование памяти — Данные генерируются и обрабатываются по одному значению, что снижает потребление памяти, так как нет необходимости хранить весь массив.
Поддержка бесконечных последовательностей — Можно генерировать бесконечные последовательности (например, числа Фибоначчи) без проблем с памятью.
Автоматическая обработка «Backpressure» — Если потребитель обрабатывает данные медленнее, генератор естественно замедляется из‑за блокировки канала, что предотвращает перегрузку памяти.
// скорость генератора будет снижаться (backpressure handling)
for line := range generateLines(bigFile) {
// за счет блокировки канала следующее значение не будет отправлено
// пока первое не прочитано из канала
processSlowly(line)
}
Частые ошибки
Забыл закрыть канал
// ПЛОХО ❌
func badGenerator() chan int {
out := make(chan int)
go func() {
for i := 1; i <= 5; i++ {
out <- i
}
// Channel never closed!
}()
return out
}
// Норм ✅
func goodGenerator() chan int {
out := make(chan int)
go func() {
defer close(out) // Always close when done
for i := 1; i <= 5; i++ {
out <- i
}
}()
return out
}
Попробуй предсказать что случится, если не закрыть канал. Как это можно исправить?
2. Без обработки ошибок
// Обработка ошибок
func generateWithErrors() (chan int, chan error) {
out := make(chan int)
errc := make(chan error, 1) // Буферизированный канал
go func() {
defer close(out)
defer close(errc)
for i := 1; i <= 5; i++ {
if i == 3 {
errc <- fmt.Errorf("error at number 3")
return
}
out <- i
}
}()
return out, errc
}
3. Утечки Ресурсов — при использовании генераторов с ресурсами (например, файлами) необходимо обеспечивать их корректное освобождение.
func generateFromFile(filename string) chan string {
out := make(chan string)
go func() {
defer close(out)
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close() // ВАЖНО все открытое - закрыть
scanner := bufio.NewScanner(file)
for scanner.Scan() {
out <- scanner.Text()
}
}()
return out
}
Это завершает наше погружение в паттерн Генератор!
Далее мы разберём конкурентный паттерн «Конвейер» (Pipeline), где научимся связывать генераторы друг с другом, создавая мощные потоки обработки данных.