Горутины и каналы в Go: Параллельное программирование
В этом уроке мы изучим горутины и каналы - мощные инструменты Go для параллельного программирования. Эти концепции позволяют писать эффективные и масштабируемые программы.
Почему важны горутины и каналы?
Горутины и каналы позволяют:
- Выполнять задачи параллельно
- Эффективно использовать ресурсы системы
- Создавать масштабируемые приложения
- Упрощать синхронизацию
- Писать чистый и понятный код
💡 Интересный факт: Горутины легче потоков операционной системы в 100-1000 раз, что позволяет создавать миллионы горутин без значительных накладных расходов.
Основы горутин
1. Простая горутина
package main
import (
"fmt"
"time"
)
// Функция, которая будет выполняться в горутине
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Printf("Число: %d\n", i)
time.Sleep(time.Millisecond * 500)
}
}
func main() {
// Запуск горутины
go printNumbers()
// Основной поток продолжает выполнение
fmt.Println("Основной поток продолжает работу...")
// Ждём, чтобы горутина успела выполниться
time.Sleep(time.Second * 3)
}
Объяснение:
- Ключевое слово
goзапускает функцию в отдельной горутине - Горутина выполняется параллельно с основным потоком
time.Sleepиспользуется для демонстрации параллельного выполнения
Ожидаемый вывод:
Основной поток продолжает работу...
Число: 1
Число: 2
Число: 3
Число: 4
Число: 5
2. Множественные горутины
package main
import (
"fmt"
"time"
)
func worker(id int) {
fmt.Printf("Работник %d начал работу\n", id)
time.Sleep(time.Second)
fmt.Printf("Работник %d закончил работу\n", id)
}
func main() {
// Запуск нескольких горутин
for i := 1; i <= 3; i++ {
go worker(i)
}
// Ждём завершения всех горутин
time.Sleep(time.Second * 2)
}
Объяснение:
- Каждая горутина выполняет свою копию функции
worker - Горутины выполняются параллельно
- Порядок вывода может варьироваться
Ожидаемый вывод:
Работник 1 начал работу
Работник 2 начал работу
Работник 3 начал работу
Работник 1 закончил работу
Работник 2 закончил работу
Работник 3 закончил работу
Основы каналов
1. Простой канал
package main
import "fmt"
func main() {
// Создание канала для передачи целых чисел
ch := make(chan int)
// Горутина для отправки данных
go func() {
ch <- 42 // Отправка значения в канал
}()
// Получение данных из канала
value := <-ch
fmt.Printf("Получено значение: %d\n", value)
}
Объяснение:
make(chan int)создаёт канал для целых чисел- Оператор
<-используется для отправки и получения данных - Канал блокирует выполнение до получения данных
Ожидаемый вывод:
Получено значение: 42
2. Буферизованный канал
package main
import "fmt"
func main() {
// Создание буферизованного канала с ёмкостью 3
ch := make(chan int, 3)
// Отправка нескольких значений
ch <- 1
ch <- 2
ch <- 3
// Получение значений
fmt.Printf("Получено: %d\n", <-ch)
fmt.Printf("Получено: %d\n", <-ch)
fmt.Printf("Получено: %d\n", <-ch)
}
Объяснение:
- Буферизованный канал может хранить несколько значений
- Отправка не блокируется, пока буфер не заполнен
- Получение происходит в порядке FIFO
Ожидаемый вывод:
Получено: 1
Получено: 2
Получено: 3
Продвинутые техники
1. Закрытие каналов
package main
import "fmt"
func main() {
ch := make(chan int)
// Горутина для отправки данных
go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch) // Закрытие канала
}()
// Чтение данных до закрытия канала
for value := range ch {
fmt.Printf("Получено: %d\n", value)
}
}
Объяснение:
close(ch)закрывает каналrangeавтоматически завершается при закрытии канала- Закрытие канала сигнализирует о завершении отправки
Ожидаемый вывод:
Получено: 1
Получено: 2
Получено: 3
Получено: 4
Получено: 5
2. Select для множественных каналов
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// Горутина для первого канала
go func() {
time.Sleep(time.Second)
ch1 <- "Сообщение из первого канала"
}()
// Горутина для второго канала
go func() {
time.Sleep(time.Second * 2)
ch2 <- "Сообщение из второго канала"
}()
// Ожидание сообщений из любого канала
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
Объяснение:
selectпозволяет ждать сообщений из нескольких каналов- Выполняется первый доступный case
- Полезно для таймаутов и обработки множественных каналов
Ожидаемый вывод:
Сообщение из первого канала
Практические задания
Задание 1: Параллельная обработка данных
Создайте программу, которая:
- Читает данные из файла
- Обрабатывает каждую строку в отдельной горутине
- Собирает результаты через канал
- Выводит статистику обработки
Ожидаемый результат:
- Эффективная обработка данных
- Корректная синхронизация
- Чёткая структура кода
Задание 2: Пул горутин
Реализуйте пул горутин для:
- Обработки задач из очереди
- Ограничения количества одновременно работающих горутин
- Сбора результатов
- Обработки ошибок
Ожидаемый результат:
- Контролируемое использование ресурсов
- Масштабируемость
- Надёжная обработка ошибок
Задание 3: Чат-сервер
Создайте простой чат-сервер, который:
- Принимает подключения от клиентов
- Передаёт сообщения между клиентами
- Управляет подключениями
- Обрабатывает отключения
Ожидаемый результат:
- Многопользовательский чат
- Устойчивость к ошибкам
- Эффективная работа с сетью
Что дальше?
В следующем уроке мы:
- Изучим более сложные паттерны синхронизации
- Познакомимся с контекстами
- Узнаем о работе с таймаутами
- Начнём писать распределённые системы
🎯 Цель урока: К концу этого урока вы должны уметь:
- Создавать и управлять горутинами
- Работать с каналами
- Использовать select для множественных каналов
- Писать параллельные программы