Перейти к содержанию
Innovialabs

Горутины и каналы в Go: руководство по конкурентности

Практика3 мин

Коротко

Горутина — лёгкая функция, запущенная через go, а канал — безопасный способ передать ей данные; завершением управляют WaitGroup и context, иначе горутины утекают.

Коротко: как устроена конкурентность в Go

Горутина — это функция, которую рантайм Go выполняет конкурентно с остальным кодом. Запускается она ключевым словом go и стоит дёшево: можно держать тысячи горутин одновременно.

Канал — типизированная труба, через которую горутины обмениваются данными. Принцип Go: не делите память для общения, а общайтесь, чтобы делить память.

func main() {
    ch := make(chan string)
    go func() {
        ch <- "готово"
    }()
    fmt.Println(<-ch)
}

Без чтения из канала main завершилась бы раньше горутины, и сообщение потерялось бы.

Небуферизованные и буферизованные каналы

ТипСозданиеПоведение
Небуферизованныйmake(chan int)Отправка ждёт, пока кто-то прочитает. Это точка синхронизации
Буферизованныйmake(chan int, 10)Отправка блокируется, только когда буфер полон

Практические правила:

  • Небуферизованный канал — когда важно, что получатель действительно принял значение.
  • Буферизованный — чтобы сгладить всплески между быстрым производителем и медленным потребителем. Буфер не лечит дедлок, а лишь откладывает его.
  • Закрывает канал отправитель, а не получатель. Чтение из закрытого канала возвращает нулевое значение, отправка в закрытый — панику.
  • for v := range ch читает до закрытия канала.

select: ожидание нескольких событий

select ждёт первый готовый из нескольких каналов. Типичное применение — таймаут:

select {
case res := <-results:
    fmt.Println(res)
case <-time.After(2 * time.Second):
    fmt.Println("таймаут")
}

Ветка default делает select неблокирующим: если ни один канал не готов, сразу выполняется она.

WaitGroup: дождаться группы горутин

Когда результат не нужен, а нужно лишь дождаться завершения, используйте sync.WaitGroup:

var wg sync.WaitGroup
for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        fetch(u)
    }(url)
}
wg.Wait()

Вызывайте Add до запуска горутины, а Done — через defer, чтобы он сработал даже при раннем выходе.

context: отмена и дедлайны

context.Context передаёт сигнал отмены вниз по цепочке вызовов. Это главный инструмент против зависших горутин:

func worker(ctx context.Context, jobs <-chan int) {
    for {
        select {
        case <-ctx.Done():
            return
        case j, ok := <-jobs:
            if !ok {
                return
            }
            process(j)
        }
    }
}

Создавайте контекст через context.WithTimeout или context.WithCancel и всегда вызывайте cancel() через defer.

Как избежать утечек горутин

Утечка — горутина, которая навсегда заблокирована и никогда не завершится. Память она не отдаёт. Частые причины:

  • Отправка в канал, который никто не читает, например после таймаута в вызывающем коде. Решение: буфер на 1 или select с ctx.Done().
  • Чтение из канала, который никто не закроет. Договоритесь, кто владелец канала и кто его закрывает.
  • Бесконечный цикл без проверки контекста.
  • Забытый wg.Done() — Wait будет ждать вечно.

Проверка: runtime.NumGoroutine() в тестах или профилировщик pprof покажут, растёт ли число горутин со временем.

Частые ошибки

  • Гонки данных при записи в общую map или срез из нескольких горутин. Запускайте тесты с флагом -race.
  • Захват переменной цикла. В современных версиях Go каждая итерация получает свою переменную, но в старом коде лучше передавать значение аргументом.
  • Неограниченный запуск горутин на каждую задачу. Для тысяч задач используйте пул воркеров с фиксированным числом горутин.

Частые вопросы

Если нужно защитить общее состояние, например счётчик или кэш, sync.Mutex проще и понятнее. Каналы удобнее, когда данные передаются от одного этапа обработки к другому.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.