Горутины без боли: рабочая ментальная модель конкурентности
Каналы как трубы, горутины как рабочие, контекст как рубильник. Простые образы, которые наконец складывают конкурентность в голове.
Конкурентность в Go пугает новичков не потому, что она сложная, а потому что объясняют её обычно терминами, а не образами. Вот модель, которая закрепляется у большинства наших студентов с первого раза.
Горутины — это рабочие на конвейере
Горутина — это не поток операционной системы, а лёгкая задача, которую планировщик Go сам распределяет между реальными потоками. Запустить горутину дёшево — можно спокойно создавать тысячи:
for _, job := range jobs {
go process(job)
}Цикл выше запускает горутины, но не ждёт их завершения — программа может закончиться раньше, чем они отработают. Нужен sync.WaitGroup или канал для синхронизации.
Каналы — это трубы между рабочими
Канал — это типизированная труба: одна горутина кладёт значение, другая забирает. Если труба пуста — читающая горутина ждёт. Если труба заполнена (для буферизированных каналов) — пишущая горутина ждёт.
results := make(chan Result, 10)
go func() {
defer close(results)
for _, job := range jobs {
results <- process(job)
}
}()
for r := range results {
handle(r)
}Контекст — это рубильник
context.Context — это способ сказать «всё, отбой» сразу всем горутинам, которые слушают этот контекст, независимо от того, как глубоко они вложены в вызовы.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case res := <-doWork(ctx):
fmt.Println(res)
case <-ctx.Done():
fmt.Println("не успели:", ctx.Err())
}Если держать в голове эти три образа — рабочие, трубы, рубильник — большинство паттернов конкурентности в Go читаются как обычная логика, а не магия.