Конкурентность в Go: горутины, каналы и async/await (сравнение)

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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                

Блоги

Рекомендаций по книгам