Почему Go обогнал всех в конкурентности?
Представьте себе шеф-повара, который готовит десять блюд одновременно. Он не бегает от плиты к плите с бешеной скоростью — он просто эффективно переключается между задачами: пока тушится мясо, нарезает салат; пока закипает вода, сервирует тарелку. В мире программирования это называется конкурентность — умение управлять множеством задач, не дожидаясь завершения каждой.
Язык Go (Golang) родился именно для решения проблем конкурентности. Его создатели из Google устали от сложностей многопоточности в C++ и Java. Они придумали горутины — легковесные потоки, которые запускаются одной строчкой кода. В этой статье мы разберём, как работает конкурентность в Go, сравним её с потоками ОС и парадигмой async/await из Python, а главное — покажем реальные примеры кода, которые вы сможете применить уже сегодня.
Горутины vs Потоки ОС: битва лёгкой кавалерии
Когда разработчик слышит «многопоточность», он обычно представляет потоки операционной системы. Но в Go всё иначе. Давайте разберём ключевые отличия.
Что такое поток ОС (Thread)?
Поток — это тяжёлый объект, которым управляет ядро ОС. Каждый поток имеет свой стек (обычно 1-8 МБ), требует системных вызовов для создания и переключения. Создать 10 000 потоков — значит убить память и процессор. В Go заложена другая философия.
Горутина (Goroutine) — лёгкий поток Go
Горутина — это функция, выполняющаяся конкурентно. Она запускается ключевым словом go. Главные фишки:
- Размер стека: начинается с 2-4 КБ и динамически растёт/сжимается.
- Создание: тысячи горутин создаются за миллисекунды.
- Планировщик: Go runtime сам распределяет горутины по потокам ОС (обычно их количество равно числу ядер CPU).
- Синтаксис: просто добавьте
goперед вызовом функции.
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Println("Привет,", name)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Запускаем две горутины
go sayHello("Go")
go sayHello("Goroutine")
// Даём время горутинам выполниться
time.Sleep(1 * time.Second)
fmt.Println("Главная функция завершена")
}
Результат: сообщения от двух горутин будут перемешаны. Go автоматически переключается между ними, не блокируя друг друга. Попробуйте запустить этот код — вы увидите, как «Привет, Go» и «Привет, Goroutine» выводятся поочерёдно.
Каналы: главный инструмент синхронизации
Горутины — это здорово, но как им общаться? В Go нет разделяемой памяти по умолчанию. Вместо этого используется принцип: «Не общайтесь, разделяя память; разделяйте память, общаясь». Для этого служат каналы (channels).
Канал — это типизированный «трубопровод», через который горутины отправляют и получают данные. Это безопасно, просто и элегантно.
Пример: параллельная обработка чисел
package main
import "fmt"
func squareWorker(numbers <-chan int, results chan<- int) {
for n := range numbers {
results <- n * n
}
close(results)
}
func main() {
numbers := make(chan int, 5)
results := make(chan int, 5)
// Запускаем воркера
go squareWorker(numbers, results)
// Отправляем данные
numbers <- 2
numbers <- 4
numbers <- 6
close(numbers)
// Читаем результаты
for res := range results {
fmt.Println("Квадрат:", res)
}
}
В этом примере мы используем буферизированные каналы (второй аргумент — размер буфера). Горутина-воркер читает из канала numbers, возводит в квадрат и отправляет в results. Закрытие канала сигнализирует, что данных больше не будет.
Конкурентность в Go vs async/await в Python
Многие Python-разработчики, переходя на Go, задаются вопросом: «А есть ли в Go async/await?». Ответ — нет, и это не недостаток, а другая модель. Давайте сравним.
Модель async/await (Python)
Python использует сопрограммы (coroutines) и цикл событий (event loop). Ключевые слова async def и await позволяют приостанавливать выполнение функции, не блокируя поток. Это однопоточная конкурентность, идеальная для I/O-bound задач (сетевые запросы, работа с файлами).
Модель горутин (Go)
Go не требует явных await. Любая функция может стать горутиной. Планировщик Go сам решает, когда переключить контекст. Для блокирующих операций (например, чтение из сети) горутина автоматически «засыпает», а другие горутины получают процессорное время.
Сравнение в таблице
- Синтаксис: Python —
async def fetch(): await response; Go —go fetch(). - Управление: Python — явное через event loop; Go — неявное через планировщик.
- Параллелизм: Python — однопоточный (GIL); Go — многопоточный (горутины на нескольких потоках ОС).
- Производительность: Go часто быстрее для CPU-bound задач, Python — удобнее для I/O.
Если вы привыкли к async/await, в Go вам понадобится немного перестроить мышление. Но как только вы освоите горутины и каналы, вы оцените их простоту и мощность.
Select: управление несколькими каналами
Часто нужно ждать данные из нескольких каналов одновременно. В Go для этого есть конструкция select — аналог switch, но для каналов.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// Горутина 1
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Ответ из канала 1"
}()
// Горутина 2
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Ответ из канала 2"
}()
select {
case msg1 := <-ch1:
fmt.Println("Получено:", msg1)
case msg