Урок 8 из 8 · 25 мин · обновлено
Горутины и каналы: первый шаг в параллельные задачи
Зачем программе делать несколько дел сразу, как запустить горутину словом go, дождаться её через WaitGroup и передавать данные по каналам. Плюс что учить дальше.
Зачем делать несколько дел сразу
Представьте сервер интернет-магазина: в одну секунду приходят сотни запросов. Если обрабатывать их по очереди, каждый будет ждать предыдущий. Или программа скачивает три файла: пока первый идёт по сети, процессор простаивает.
В Go несколько задач сразу выполняют горутины — лёгкие потоки работы. Запустить их можно тысячами: каждая занимает всего несколько килобайт памяти. Это одна из главных причин, почему Go так любят для серверов.
Горутины и WaitGroup
Горутину запускают словом go перед вызовом функции. Программа не ждёт её окончания и сразу идёт дальше. Но когда завершается main, завершаются и все горутины, даже недоработавшие. Поэтому их нужно дождаться. Для этого есть sync.WaitGroup:
func download(name string) {
time.Sleep(100 * time.Millisecond) // делаем вид, что скачиваем
fmt.Println("Готово:", name)
}
func main() {
start := time.Now()
var wg sync.WaitGroup
for _, f := range []string{"a.pdf", "b.pdf", "c.pdf"} {
wg.Go(func() {
download(f)
})
}
wg.Wait()
fmt.Println("Все файлы за", time.Since(start).Round(100*time.Millisecond))
}
Готово: b.pdf
Готово: a.pdf
Готово: c.pdf
Все файлы за 100ms
wg.Go(func() { … })запускает функцию в горутине и записывает её в группу;wg.Wait()ждёт, пока закончат все горутины группы;func() { … }— функция без имени, её пишут прямо на месте.
Каждый файл «скачивается» 100 миллисекунд, а все три вместе заняли столько же, а не 300: горутины работали одновременно. Порядок строк «Готово» при каждом запуске может быть разным — какая горутина закончит первой, заранее неизвестно.
Каналы
Горутинам часто нужно передать результат. Для этого есть каналы — трубы, по которым одна горутина отправляет значение, а другая его принимает:
ch := make(chan string)
go func() {
ch <- "привет из горутины"
}()
msg := <-ch
fmt.Println(msg)
привет из горутины
make(chan string)создаёт канал для строк;ch <- значение— отправить в канал;<-ch— принять из канала. Если значения ещё нет, горутина ждёт.
Это ожидание и синхронизирует работу: main не идёт дальше, пока не получит сообщение. Отправитель тоже ждёт, пока кто-то примет значение.
Когда значений несколько, отправитель после последнего закрывает канал через close, а получатель перебирает канал через range:
nums := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
nums <- i * 10
}
close(nums)
}()
for n := range nums {
fmt.Println(n)
}
10
20
30
Если все горутины ждут и никто не может продолжить, Go останавливает программу с ошибкой fatal error: all goroutines are asleep - deadlock!. Например, когда main отправляет в канал, а принять некому. Встретив такое сообщение, ищите, кто кого ждёт.
Куда дальше
Вы прошли основу языка. Go-разработчику на первой работе обычно нужны:
- веб-серверы и API —
net/httpи формат JSON, позже популярные надстройки вроде chi или gin; - базы данных — язык запросов SQL и работа с PostgreSQL из Go. Начать можно с курса «SQL с нуля»;
- тесты — в Go они встроены: файлы
_test.goи командаgo test; - Git и Docker — без них не обходится ни одна команда. Есть курсы «Git и GitHub» и «Docker для начинающих».
Хорошая цель для портфолио — небольшой сервис: например, API списка задач, который хранит данные в PostgreSQL, покрыт тестами и запускается в Docker.
Попробуйте сами. Напишите функцию square(n int, ch chan int), которая отправляет в канал квадрат числа. Запустите её в трёх горутинах для чисел 1, 2 и 3, примите три результата и выведите их сумму.
Решение
package main
import "fmt"
func square(n int, ch chan int) {
ch <- n * n
}
func main() {
res := make(chan int)
for i := 1; i <= 3; i++ {
go square(i, res)
}
sum := 0
for range 3 {
sum += <-res
}
fmt.Println(sum)
}
14
Результаты приходят в любом порядке, но для суммы это не важно: 1 + 4 + 9 = 14. Ровно три приёма из канала гарантируют, что main дождётся все горутины, и WaitGroup здесь не нужен.
Мини-тест: проверьте себя
Закрепите урок: несколько вопросов, разбор ответов — в конце.
-
Что делает запись go download("a.pdf")?
Слово go запускает функцию одновременно с остальным кодом. Программа не ждёт её окончания, а переходит к следующей строке.
-
Зачем в примере с файлами нужен wg.Wait()?
Без ожидания main может закончиться раньше горутин, и они не успеют доработать. wg.Wait() блокирует программу, пока не завершатся все задачи группы.
-
Что произойдёт при выполнении строки msg := <-ch?
Стрелка слева от канала — приём. Если значения ещё нет, горутина ждёт. Так каналы и синхронизируют работу.
-
Три горутины выводят по строке. В каком порядке появятся строки?
Горутины работают одновременно, и Go сам решает, какую когда выполнять. Если порядок важен, его задают каналами или собирают результаты и сортируют.