Урок 9. Горутины и каналы
Сервер обслуживает сотни клиентов сразу, программа скачивает десять файлов одновременно — для таких задач в Go есть горутины. Горутина — функция, которая выполняется одновременно с остальной программой. Она гораздо легче потока операционной системы (обычного способа выполнять части программы одновременно): программа на Go спокойно держит десятки тысяч горутин.
Запуск горутины
Горутину запускает слово go перед вызовом функции. Но есть ловушка: программа заканчивается, когда заканчивается main, и не ждёт остальные горутины. В такой программе горутина, скорее всего, не успеет ничего напечатать:
package main
import "fmt"
func main() {
go fmt.Println("из горутины")
fmt.Println("из main")
}Выведется «из main», а «из горутины» — как повезёт. Ждать горутины нужно явно.
Ожидание: sync.WaitGroup
sync.WaitGroup — счётчик запущенных задач. Add(1) перед запуском увеличивает его, Done() в конце горутины уменьшает, Wait() ждёт, пока счётчик не станет нулём:
package main
import (
"fmt"
"sync"
)
func square(n int) int {
return n * n
}
func main() {
nums := []int{2, 3, 4, 5}
results := make([]int, len(nums))
var wg sync.WaitGroup
for i, n := range nums {
wg.Add(1)
go func() {
defer wg.Done()
results[i] = square(n)
}()
}
wg.Wait()
fmt.Println(results)
}[4 9 16 25]go func() { … }()— запуск анонимной функции горутиной; скобки()в конце — её вызов.defer wg.Done()гарантирует, чтоDoneвызовется при выходе из функции, даже если в ней случится паника (оdefer— в справке defer).- Каждая горутина пишет в свою ячейку
results[i], поэтому они не мешают друг другу, а результат послеWaitвсегда одинаковый, хотя горутины заканчивают в разном порядке.
С Go 1.22 переменные цикла i и n создаются заново на каждом шаге, и каждая горутина видит свои. В старых версиях (и если в go.mod указана версия ниже 1.22) все горутины видели одну переменную, и результат был неверным — в старом коде поэтому встречается i, n := i, n в начале цикла.
Каналы
Канал — труба, по которой горутины передают друг другу значения. Создаётся через make(chan тип); отправка — ch <- v, получение — v := <-ch.
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println("получено", v)
}
fmt.Println("канал закрыт")
}получено 1
получено 2
получено 3
канал закрыт- Обычный канал без буфера: отправка ждёт, пока кто-то не примет значение, а приём ждёт, пока кто-то не отправит. Так горутины сами синхронизируются, и значения приходят ровно в порядке отправки.
close(ch)сообщает, что значений больше не будет. Закрывает канал тот, кто отправляет.for v := range chпринимает значения, пока канал не закроют. Безcloseцикл ждал бы вечно.
Буферизованный канал и сбор результатов
make(chan int, 5) — канал с буфером на 5 значений: отправка не ждёт получателя, пока в буфере есть место. Удобно собирать результаты нескольких горутин:
package main
import (
"fmt"
"sync"
)
func square(n int) int {
return n * n
}
func main() {
jobs := []int{1, 2, 3, 4, 5}
results := make(chan int, len(jobs))
var wg sync.WaitGroup
for _, n := range jobs {
wg.Add(1)
go func() {
defer wg.Done()
results <- square(n)
}()
}
wg.Wait()
close(results)
sum := 0
for r := range results {
sum += r
}
fmt.Println("сумма квадратов:", sum)
}сумма квадратов: 55Квадраты приходят в канал в любом порядке, но сумма от порядка не зависит. Буфера хватает на все результаты, поэтому горутины не застревают на отправке, пока main ждёт в Wait. Канал закрывают после Wait — когда точно никто больше не отправит.
select
select ждёт сразу несколько каналов и выполняет ветку того, который готов первым. Частый пример — ожидание с ограничением времени:
select {
case res := <-results:
fmt.Println("результат:", res)
case <-time.After(2 * time.Second):
fmt.Println("слишком долго")
}time.After возвращает канал, в который через заданное время придёт значение. Если результат не успел за две секунды, сработает вторая ветка.
Гонка данных и sync.Mutex
Если несколько горутин одновременно меняют одну переменную, получается гонка данных (data race): count++ — это «прочитать, прибавить, записать», и горутины затирают результаты друг друга. Итог каждый раз разный. Защищают общие данные мьютексом — замком, который в каждый момент держит только одна горутина:
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
var wg sync.WaitGroup
count := 0
for range 1000 {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(count)
}1000Без mu.Lock() и mu.Unlock() программа печатала бы разные числа меньше 1000. Найти гонки помогает встроенный детектор: запустите программу командой go run -race . — при гонке он напечатает WARNING: DATA RACE и места в коде.
Известная поговорка Go: «не общайтесь через общую память — делитесь памятью через общение». Если данные можно передать по каналу, это обычно проще и надёжнее мьютекса.
Deadlock
Deadlock — взаимная блокировка: все горутины ждут друг друга, и ни одна не может продолжить. Тогда Go останавливает программу:
fatal error: all goroutines are asleep - deadlock!Чаще всего это отправка в канал без буфера, когда никто не принимает, или range по каналу, который забыли закрыть. Разбор причин — в справке all goroutines are asleep — deadlock.
Коротко
go f()запускает функцию горутиной;mainгорутины не ждёт.sync.WaitGroup:Add(1)до запуска,defer wg.Done()в горутине,Wait()в конце.- Канал:
make(chan T), отправкаch <- v, приём<-ch;closeиrange— перебор до закрытия. - Без буфера отправка ждёт получателя;
make(chan T, n)— буфер наnзначений. - Общую переменную из нескольких горутин меняют под
sync.Mutex; проверка —go run -race.