Паттерн Конкурентного генератора в Go: Наглядное руководство
- четверг, 13 февраля 2025 г. в 00:00:14
Итак вторая часть продолжаем. Теперь давайте посмотрим, как эти примитивы объединяются, образуя мощные шаблоны, которые решают реальные проблемы.
В этой статье мы рассмотрим генератор и попытаемся визуализировать его. Итак, давайте подготовимся, поскольку весь процесс пройдем с примерами.
Генератор как фонтан, который непрерывно производит значения, которые мы можем использовать при необходимости.
В 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), где научимся связывать генераторы друг с другом, создавая мощные потоки обработки данных.