Урок 8 из 8 · 25 мин · обновлено

Горутины и каналы: первый шаг в параллельные задачи

Зачем программе делать несколько дел сразу, как запустить горутину словом go, дождаться её через WaitGroup и передавать данные по каналам. Плюс что учить дальше.

Зачем делать несколько дел сразу

Представьте сервер интернет-магазина: в одну секунду приходят сотни запросов. Если обрабатывать их по очереди, каждый будет ждать предыдущий. Или программа скачивает три файла: пока первый идёт по сети, процессор простаивает.

В Go несколько задач сразу выполняют горутины — лёгкие потоки работы. Запустить их можно тысячами: каждая занимает всего несколько килобайт памяти. Это одна из главных причин, почему Go так любят для серверов.

Горутины и WaitGroup

Горутину запускают словом go перед вызовом функции. Программа не ждёт её окончания и сразу идёт дальше. Но когда завершается main, завершаются и все горутины, даже недоработавшие. Поэтому их нужно дождаться. Для этого есть sync.WaitGroup:

Go
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: горутины работали одновременно. Порядок строк «Готово» при каждом запуске может быть разным — какая горутина закончит первой, заранее неизвестно.

Каналы

Горутинам часто нужно передать результат. Для этого есть каналы — трубы, по которым одна горутина отправляет значение, а другая его принимает:

Go
ch := make(chan string)
go func() {
	ch <- "привет из горутины"
}()
msg := <-ch
fmt.Println(msg)
Результат
привет из горутины
  • make(chan string) создаёт канал для строк;
  • ch <- значение — отправить в канал;
  • <-ch — принять из канала. Если значения ещё нет, горутина ждёт.

Это ожидание и синхронизирует работу: main не идёт дальше, пока не получит сообщение. Отправитель тоже ждёт, пока кто-то примет значение.

Когда значений несколько, отправитель после последнего закрывает канал через close, а получатель перебирает канал через range:

Go
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, примите три результата и выведите их сумму.

Решение
Go
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 здесь не нужен.

Мини-тест: проверьте себя

Закрепите урок: несколько вопросов, разбор ответов — в конце.

  1. Что делает запись go download("a.pdf")?

  2. Зачем в примере с файлами нужен wg.Wait()?

  3. Что произойдёт при выполнении строки msg := <-ch?

  4. Три горутины выводят по строке. В каком порядке появятся строки?