Коротко: горутина — очень лёгкая нить выполнения, канал — безопасная труба для обмена данными; вместе дают конкурентность без ручной возни с потоками.
Если совсем коротко: горутина — это очень лёгкая «нить» выполнения, которую 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 в высоконагруженных и сетевых задачах. Если проектируете сервис, где важна конкурентность, и не уверены, какой инструмент выбрать, — это хорошая тема для консультации.


Комментарии