Горутина (goroutine) в Go, Каналы (channels) в Go и Конвейеры (Pipelines) в Go.
Categories:
goroutine
Горутина (goroutine) в Go — это легковесный “поток”, который позволяет выполнять код конкурентно (почти одновременно) без создания полноценных потоков ОС.Простыми словами о Goroutine:
- Это как виртуальный работник, который выполняет задачи параллельно с другими горутинами.
- Горутины дешевле настоящих потоков (их можно создавать тысячи без нагрузки на систему).
- Управляются средой Go (runtime), а не операционной системой.
Пример:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Привет!")
}
func main() {
// Запуск горутины (выполняется параллельно с main)
go sayHello()
// Главная функция продолжает работу
fmt.Println("Главная функция...")
time.Sleep(1 * time.Second) // Ждём, чтобы горутина успела выполниться
}
Вывод:
Главная функция...
Привет!
Особенности:
- Легковесные — занимают всего ~2 КБ памяти (потоки ОС — от 1 МБ).
- Быстрый старт — создаются и запускаются почти мгновенно.
- Автомасштабирование — Go сам распределяет их по доступным ядрам CPU.
- Не блокируют main — если главная функция завершится, все горутины умрут.
Важно:
- Горутины не гарантируют порядок выполнения.
- Для обмена данными между горутинами используют каналы (channels).
Аналог из жизни:
Представьте, что вы (главная функция) даёте задание коллеге (горутине):
- Вы можете продолжать работать, пока коллега выполняет свою часть.
- Вам не нужно ждать его — всё происходит конкурентно.
Горутины — одна из главных “фишек” Go, делающая его мощным для многозадачных программ!
channels
Каналы (channels) в Go — это специальный тип данных, который позволяет безопасно передавать данные между горутинами.Представьте их как трубы, по которым одна горутина отправляет данные, а другая — принимает. Это помогает избежать гонок данных (data races) и синхронизировать работу горутин.
Базовые операции с каналами
1. Создание канала
Каналы бывают:
- Буферизированные (с фиксированной ёмкостью)
- Небуферизированные (ждёт, пока получатель заберёт данные)
ch := make(chan int) // Небуферизированный канал для int
chBuf := make(chan int, 3) // Буферизированный (ёмкость = 3)
2. Отправка данных (<-
)
ch <- 42 // Отправить число 42 в канал
3. Получение данных (<-
)
value := <-ch // Получить данные из канала
fmt.Println(value) // 42
4. Закрытие канала (close
)
Закрытый канал нельзя использовать для отправки, но можно читать оставшиеся данные.
close(ch)
Пример: Простая передача данных
package main
import "fmt"
func sendData(ch chan string) {
ch <- "Привет из горутины!" // Отправляем данные
}
func main() {
ch := make(chan string) // Создаём канал
go sendData(ch) // Запускаем горутину
msg := <-ch // Получаем данные
fmt.Println(msg) // "Привет из горутины!"
close(ch) // Закрываем канал
}
Вывод:
Привет из горутины!
Важные особенности каналов
1. Блокировка операций
-
Небуферизированный канал:
- Отправка (
ch <- data
) блокирует горутину, пока другая горутина не прочитает данные. - Чтение (
<-ch
) блокирует, пока не поступят данные.
- Отправка (
-
Буферизированный канал:
- Отправка блокируется только если буфер заполнен.
- Чтение блокируется только если буфер пуст.
2. Проверка на закрытие
value, ok := <-ch
if !ok {
fmt.Println("Канал закрыт!")
}
3. range
по каналу
Можно перебирать данные, пока канал не закроется:
for msg := range ch {
fmt.Println(msg)
}
Пример: Синхронизация горутин
package main
import (
"fmt"
"time"
)
func worker(done chan bool) {
fmt.Println("Работаю...")
time.Sleep(2 * time.Second)
fmt.Println("Готово!")
done <- true // Сигнализируем о завершении
}
func main() {
done := make(chan bool)
go worker(done)
<-done // Ждём сигнал от worker
fmt.Println("Рабочая горутина завершилась.")
}
Вывод:
Работаю...
Готово!
Рабочая горутина завершилась.
Когда использовать каналы?
- Для синхронизации (ожидание завершения горутин).
- Для передачи данных между горутинами.
- Для организации конвейеров (pipeline) обработки данных.
Ошибки при работе с каналами
- Deadlock — если нет получателя/отправителя.
ch := make(chan int) ch <- 42 // Ошибка: никто не читает из канала!
- Паника при отправке в закрытый канал.
close(ch) ch <- 10 // panic: send on closed channel
- Чтение из пустого закрытого канала вернёт нулевое значение.
Аналог из жизни
Канал — как почтовый ящик:
- Вы (горутина) кладёте письмо (
ch <- data
). - Другой человек (горутина) забирает его (
<-ch
). - Если ящик переполнен (буфер заполнен), вы ждёте, пока освободится место.
Каналы — это безопасный и удобный способ общения между горутинами в Go!
Pipelines
Конвейеры (Pipelines) в Go: примеры и особенностиКонвейер (pipeline) — это цепочка горутин, где каждая выполняет свою часть работы, передавая данные следующей через каналы. Это мощный паттерн для:
- Обработки данных по этапам (фильтрация, преобразование, агрегация).
- Параллельного выполнения задач с контролируемым потоком данных.
- Управления ресурсами (ограничение числа одновременно работающих горутин).
Базовый конвейер (3 этапа)
1. Генерация данных → Обработка → Вывод
package main
import "fmt"
// Этап 1: Генерация чисел (producer)
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// Этап 2: Умножение (processor)
func multiply(in <-chan int, factor int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * factor
}
close(out)
}()
return out
}
// Этап 3: Вывод (consumer)
func print(in <-chan int) {
for n := range in {
fmt.Println(n)
}
}
func main() {
// Конвейер: generate → multiply → print
data := generate(1, 2, 3, 4)
processed := multiply(data, 10)
print(processed)
}
Вывод:
10
20
30
40
Особенности конвейеров
1. Каждый этап — отдельная горутина
- Этапы работают параллельно (если CPU позволяет).
- Данные передаются последовательно через каналы.
2. Закрытие каналов
- Когда producer закрывает канал, все processors и consumer получают сигнал завершения.
- Использование
range
для чтения из канала автоматически завершается приclose()
.
3. Буферизация для ускорения
Буферизированные каналы уменьшают блокировки:
out := make(chan int, 100) // Буфер на 100 элементов
Фильтрация в конвейере
Добавим этап, который оставляет только чётные числа:
// Этап 2.5: Фильтрация чётных чисел
func filterEven(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
if n%2 == 0 {
out <- n
}
}
close(out)
}()
return out
}
func main() {
data := generate(1, 2, 3, 4, 5, 6)
processed := multiply(data, 3) // Умножаем на 3
evenOnly := filterEven(processed) // Оставляем чётные
print(evenOnly)
}
Вывод:
6
12
18
Fan-Out/Fan-In: Параллельная обработка
- Fan-Out: Разделение работы между несколькими горутинами.
- Fan-In: Объединение результатов в один канал.
Пример:
// Fan-In: объединяет несколько каналов в один
func merge(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
// Запускаем для каждого канала горутину, которая перенаправляет данные в out
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}(ch)
}
// Закрываем out после завершения всех горутин
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
data := generate(1, 2, 3, 4, 5, 6, 7, 8)
// Fan-Out: Запускаем 3 обработчика
proc1 := multiply(data, 10)
proc2 := multiply(data, 20)
proc3 := multiply(data, 30)
// Fan-In: Объединяем результаты
merged := merge(proc1, proc2, proc3)
print(merged)
}
Вывод (порядок может меняться):
10
20
30
40
50
60
70
80
20
40
60
80
100
120
140
160
30
60
90
120
150
180
210
240
Ошибки и подводные камни
-
Утечки горутин
- Если канал не закрыт, горутина может зависнуть в ожидании.
- Всегда закрывайте каналы на стороне отправителя.
-
Deadlock
- Если канал не читается, отправляющая горутина заблокируется навсегда.
-
Ограничение параллелизма
- Если запустить 1000 горутин, они могут перегрузить систему.
- Решение: пул воркеров с использованием
semaphore
илиbuffered channels
.
Когда использовать конвейеры?
- ETL-процессы (извлечение, преобразование, загрузка данных).
- Параллельная обработка большого количества задач (например, скачивание файлов).
- Построение многопоточных приложений с чёткой структурой.
Аналог из жизни
Конвейер — как заводская линия сборки:
- Этап 1: Детали поступают на ленту (генерация данных).
- Этап 2: Робот красит детали (обработка).
- Этап 3: Другой робот упаковывает (фильтрация).
- Этап 4: Готовые изделия отправляются на склад (вывод).
Каждый этап работает независимо, но согласованно через общий механизм (каналы).
Итог
Конвейеры в Go — это:
✅ Гибкость (можно добавлять/удалять этапы).
✅ Параллелизм (эффективное использование CPU).
✅ Безопасность (данные передаются через каналы, нет гонок).
Используйте их для сложных многопоточных задач!