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

Если совсем коротко: горутина — это очень лёгкая «нить» выполнения, которую Go запускает почти бесплатно, а канал — это безопасная труба, по которой горутины обмениваются данными. Вместе они дают конкурентность без ручной возни с потоками и блокировками. Разберём по шагам, с кодом и типичными ошибками.

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

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

func say(text string) {
    fmt.Println(text)
}

func main() {
    go say("привет из горутины") // запуск в фоне
    say("привет из main")
    time.Sleep(time.Millisecond) // грубо подождём, чтобы фоновая успела
}

Здесь say("привет из горутины") уйдёт в фон, а main продолжит работу. Проблема в том, что main может завершиться раньше, чем отработает горутина — и программа просто закончится. Грубый time.Sleep — это костыль; правильный способ синхронизации — каналы и WaitGroup.

Зачем нужны каналы

Несколько горутин, читающих и пишущих одни и те же данные, легко устроят гонку (race condition). В Go предпочитают другой подход — формулировку из официального девиза: «Не общайтесь через разделяемую память; разделяйте память через общение». То есть данные передаются по каналам, а не лежат в общей переменной под замком.

Каналы: отправка и получение

Канал создаётся через make, отправка и получение — оператором <-:

ch := make(chan string)

go func() {
    ch <- "готово" // отправляем в канал
}()

msg := <-ch // получаем (блокируется, пока не придёт значение)
fmt.Println(msg)

Небуферизированный канал синхронен: отправка ждёт получателя, а получение — отправителя. Это и есть встроенная синхронизация: код после <-ch гарантированно выполнится уже после того, как горутина отправила значение.

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

Если указать размер буфера, отправка не блокируется, пока буфер не заполнен:

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

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

Закрытие канала и range

Отправитель может закрыть канал через close, а получатель — читать его в цикле range, пока канал не закрыт:

ch := make(chan int)

go func() {
    for i := 0; i < 3; i++ {
        ch <- i
    }
    close(ch) // закрывает ТОЛЬКО отправитель
}()

for v := range ch { // цикл завершится сам после close
    fmt.Println(v)
}

Важное правило: закрывает канал всегда отправитель, и никогда нельзя писать в закрытый канал — это паника.

select — ожидание нескольких каналов

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

select {
case msg := <-ch1:
    fmt.Println("из ch1:", msg)
case msg := <-ch2:
    fmt.Println("из ch2:", msg)
case <-time.After(time.Second):
    fmt.Println("таймаут: никто не ответил за секунду")
}

WaitGroup — дождаться всех горутин

Когда нужно запустить пачку горутин и дождаться их завершения, удобнее sync.WaitGroup, чем каналы:

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done() // отметим завершение
        fmt.Println("обработал", n)
    }(i)
}

wg.Wait() // блокируется, пока все не вызвали Done()

Обратите внимание на (i) при вызове: переменную цикла передаём аргументом, иначе все горутины могут увидеть одно и то же финальное значение.

Типичные ошибки новичка

  • Deadlock. Чтение из канала, в который никто не пишет (и наоборот) — программа зависнет с fatal error: all goroutines are asleep - deadlock.
  • Утечка горутин (goroutine leak). Горутина навсегда заблокирована на канале, который никто не закроет, — она остаётся в памяти. Помогает context для отмены.
  • Запись в закрытый канал. Паника. Закрывает только отправитель и только один раз.
  • Гонки данных. Если всё-таки используете общую память, защищайте её через sync.Mutex и проверяйте детектором гонок: go run -race .

Когда что использовать

  • Каналы — для передачи данных и конвейеров (pipeline) между горутинами.
  • WaitGroup — когда нужно просто дождаться завершения группы задач.
  • Mutex — когда без общей памяти не обойтись и нужно защитить доступ.

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