Асинхронное программирование в Go: полное руководство для начинающих

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

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

Начать курс

Асинхронное программирование в Go: полное руководство для начинающих

Асинхронное программирование — это подход, при котором программа может выполнять несколько задач одновременно, не блокируя выполнение основного потока. В языке Go (Golang) этот механизм реализован особенно элегантно и просто благодаря встроенным примитивам: горутинам (goroutines) и каналам (channels). В отличие от многих других языков, где асинхронность требует сложных колбэков или промисов, Go предлагает лаконичный синтаксис и мощные инструменты для работы с конкурентностью.

В этом руководстве мы разберём основы асинхронного программирования в Go. Вы узнаете, что такое горутины, как создавать и синхронизировать их с помощью каналов, а также как использовать оператор select для работы с несколькими каналами. Материал рассчитан на начинающих, поэтому все примеры будут максимально простыми и понятными.

Что такое горутины и как они работают

Горутина (goroutine) — это легковесный поток выполнения, который управляется рантаймом Go. В отличие от обычных потоков операционной системы, горутины потребляют гораздо меньше ресурсов: стек горутины начинается с нескольких килобайт и может динамически расширяться. Запустить горутину можно простым добавлением ключевого слова go перед вызовом функции:

package main

import (
    "fmt"
    "time"
)

func printMessage(msg string) {
    for i := 0; i < 3; i++ {
        fmt.Println(msg)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // Запускаем горутину
    go printMessage("Привет из горутины!")
    
    // Основная функция
    printMessage("Привет из main!")
    
    // Ждём завершения горутины
    time.Sleep(1 * time.Second)
}

В этом примере функция printMessage запускается дважды: один раз как обычная функция, второй — как горутина. Выполнение происходит конкурентно: сообщения будут перемешиваться. Обратите внимание: без time.Sleep в конце main программа завершится, не дождавшись выполнения горутины. Для надёжной синхронизации лучше использовать специальные механизмы.

Каналы: безопасная передача данных между горутинами

Каналы (channels) — это типизированные "трубы", через которые горутины могут обмениваться данными. Канал создаётся с помощью функции make и оператора chan. Данные отправляются в канал через <-, а принимаются из него аналогичным синтаксисом.

Рассмотрим базовый пример:

package main

import "fmt"

func sendData(ch chan string) {
    ch <- "Данные из горутины"
}

func main() {
    // Создаём канал для строк
    messageChannel := make(chan string)
    
    // Запускаем горутину, которая отправляет данные
    go sendData(messageChannel)
    
    // Принимаем данные из канала (блокируется до получения)
    message := <-messageChannel
    
    fmt.Println(message)
}

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

Каналы могут быть буферизированными. Буфер позволяет отправлять данные без блокировки, пока буфер не заполнится:

// Создаём буферизированный канал на 2 элемента
bufferedChannel := make(chan string, 2)

// Отправляем данные без блокировки
bufferedChannel <- "Первое"
bufferedChannel <- "Второе"

// Теперь можно принять данные
fmt.Println(<-bufferedChannel)
fmt.Println(<-bufferedChannel)

Оператор select: работа с несколькими каналами

Когда у вас есть несколько каналов, и вы хотите реагировать на тот, который первым готов к обмену данными, используйте оператор select. Он работает аналогично switch, но для каналов:

package main

import (
    "fmt"
    "time"
)

func main() {
    channel1 := make(chan string)
    channel2 := make(chan string)
    
    // Первая горутина отправляет данные через 1 секунду
    go func() {
        time.Sleep(1 * time.Second)
        channel1 <- "Сообщение из канала 1"
    }()
    
    // Вторая горутина отправляет данные через 2 секунды
    go func() {
        time.Sleep(2 * time.Second)
        channel2 <- "Сообщение из канала 2"
    }()
    
    // Ожидаем данные из любого канала
    select {
    case msg1 := <-channel1:
        fmt.Println("Получено:", msg1)
    case msg2 := <-channel2:
        fmt.Println("Получено:", msg2)
    case <-time.After(3 * time.Second):
        fmt.Println("Таймаут! Ни один канал не ответил за 3 секунды")
    }
}

В этом примере select выберет тот канал, который первым будет готов к чтению. Если готовы оба, выбор происходит случайным образом. Блок time.After создаёт канал, который отправляет значение через указанное время — это удобный способ реализовать таймаут.

Синхронизация и паттерны: WaitGroup и мьютексы

Иногда каналы избыточны, и вам нужно просто дождаться завершения нескольких горутин. Для этого используйте sync.WaitGroup:

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Уменьшаем счётчик при завершении
    fmt.Printf("Работник %d начал работу\n", id)
    // Имитация работы
    fmt.Printf("Работник %d завершил работу\n", id)
}

func main() {
    var wg sync.WaitGroup
    
    for i := 1; i <= 3; i++ {
        wg.Add(1) // Увеличиваем счётчик
        go worker(i, &wg)
    }
    
    wg.Wait() // Блокируемся, пока счётчик не станет 0
    fmt.Println("Все работники завершили работу")
}

Для защиты общих данных от одновременного изменения используйте sync.Mutex:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {                

Блоги

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