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

Backend2026-09-30TryQuickToolBox

Вы наверняка писали код на Go, который работает нормально — пока не перестанет: может быть, гонка данных обрушивает сервис под нагрузкой, или взаимная блокировка замораживает API. Конкурентность — одно из главных преимуществ Go, но именно на ней многие разработчики застревают. Горутины и каналы — это строительные блоки, но для их правильного использования нужно понимать их поведение и типичные ошибки.

Это руководство описывает практические паттерны использования горутин и каналов с примерами кода и советами, которые можно применить сразу.

Что такое горутины?

Горутина — это легковесный поток, управляемый средой выполнения Go. Вы создаёте её, добавляя префикс go к вызову функции:

go doSomething()

Горутины начинаются с небольшого стека (несколько килобайт), который растёт по мере необходимости. Это делает их создание дешёвым — можно порождать тысячи и даже миллионы горутин. Планировщик Go мультиплексирует горутины на потоки ОС, так что вы не управляете потоками напрямую.

Но запуск горутины без координации — рецепт хаоса. Нужен способ дождаться их завершения, передать данные или отменить их — и здесь на сцену выходят каналы.

Каналы: коммуникация между горутинами

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

Создайте канал с помощью make(chan T):

ch := make(chan int)
go func() {
    ch <- 42 // отправка
}()
value := <-ch // получение

По умолчанию каналы небуферизованные, то есть отправка и получение происходят синхронно. Буферизованные каналы позволяют выполнить ограниченное число отправок без получателя:

ch := make(chan int, 3) // размер буфера 3
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 заблокируется, пока кто-нибудь не получит

Буферизованные каналы сглаживают всплески, но не устраняют необходимость координации.

Распространённые паттерны конкурентности

1. Пул воркеров

Распределяйте задачи между фиксированным числом воркеров, чтобы ограничить использование ресурсов:

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for i := 1; i <= 5; i++ {
        jobs <- i
    }
    close(jobs)

    for r := 1; r <= 5; r++ {
        <-results
    }
}

Этот паттерн отлично подходит для задач, ограниченных CPU или IO, где вы хотите контролировать параллелизм.

2. Fan-Out, Fan-In

Fan-out означает, что несколько горутин читают из одного канала. Fan-in — объединение результатов из нескольких каналов в один:

func fanIn(chs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    wg.Add(len(chs))
    for _, ch := range chs {
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

Используйте это, когда у вас несколько производителей и нужен единый поток результатов.

3. Select для мультиплексирования

Оператор select позволяет горутине ожидать несколько операций с каналами:

select {
case msg := <-ch1:
    fmt.Println("получено из ch1:", msg)
case ch2 <- 42:
    fmt.Println("отправлено в ch2")
case <-time.After(1 * time.Second):
    fmt.Println("таймаут")
}

Используйте select с веткой default для неблокирующих операций или с таймаутом, чтобы не ждать вечно.

Подводные камни и как их избежать

Лучшие практики

Сравнение: каналы против мьютексов

АспектКаналыМьютексы
Случай использованияПередача данных, координацияЗащита общего состояния
СложностьВыше для простых задачНиже для простых задач
ПроизводительностьНакладные расходы на синхронизациюОбычно быстрее для коротких критических секций
РискДедлоки, утечкиДедлоки при неосторожности

FAQ

Когда следует использовать буферизованный канал?

Используйте буферизованный канал, когда хотите развязать отправителей и получателей для обработки всплесков, или когда вы знаете точное число отправок и хотите избежать блокировки. Однако небуферизованные каналы дают более сильные гарантии синхронизации и часто проще для рассуждений.

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

Убедитесь, что у каждой горутины есть чёткий путь выхода. Используйте context.Context для сигнала отмены, закрывайте каналы, когда больше не будет отправок, и избегайте бесконечной блокировки на операциях с каналами без таймаута или ветки default.

Можно ли использовать каналы для всего?

Нет. Хотя каналы мощны, они добавляют накладные расходы и сложность. Для простого общего состояния мьютекс часто более прямолинеен и эффективен. Поговорка Go гласит: «Не общайтесь через разделяемую память; разделяйте память через общение», но это не значит, что каналы — всегда ответ.

Если вы отлаживаете проблемы конкурентности или анализируете логи ваших Go-сервисов, Nginx Log Analyzer поможет быстро находить паттерны и ошибки.