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

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

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

Начать курс

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

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

Что такое горутины и чем они отличаются от потоков?

Горутина — это легковесный поток выполнения, управляемый рантаймом Go. В отличие от системных потоков ОС, горутины потребляют гораздо меньше ресурсов: их стек начинается с нескольких килобайт и может динамически расти и уменьшаться. Это позволяет запускать миллионы горутин одновременно без значительной нагрузки на память.

Запустить горутину очень просто — достаточно добавить ключевое слово go перед вызовом функции:

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Привет из горутины!")
}

func main() {
    go sayHello() // Запуск горутины
    time.Sleep(100 * time.Millisecond) // Ждём, чтобы горутина успела выполниться
    fmt.Println("Главная функция завершена")
}

Важно отметить: если главная функция main() завершится, все запущенные горутины будут принудительно остановлены. Для синхронизации мы используем специальные инструменты.

Синхронизация с помощью WaitGroup

Простейший способ дождаться завершения горутин — использовать sync.WaitGroup. Он работает как счётчик: мы увеличиваем его при запуске горутины и уменьшаем при завершении. Главная функция блокируется до тех пор, пока счётчик не станет равен нулю.

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Уменьшаем счётчик при завершении
    fmt.Printf("Работник %d начал работу\n", id)
    time.Sleep(time.Second)
    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() // Ждём завершения всех горутин
    fmt.Println("Все работники завершили работу")
}

Обратите внимание: wg.Add(1) вызывается до запуска горутины, а wg.Done() — внутри неё (обычно через defer). Это гарантирует, что счётчик будет корректно уменьшен даже при панике.

Каналы: общение между горутинами

Каналы (channels) — это типизированные «трубы», через которые горутины могут безопасно обмениваться данными. Каналы обеспечивают синхронизацию по умолчанию: отправка в канал блокирует горутину до тех пор, пока другая горутина не прочитает значение, и наоборот.

Создаются каналы с помощью функции make:

package main

import "fmt"

func main() {
    // Создаём небуферизированный канал для целых чисел
    ch := make(chan int)

    // Запускаем горутину, которая отправляет данные
    go func() {
        ch <- 42 // Отправляем значение в канал
    }()

    // Читаем из канала (блокируется, пока не придёт значение)
    value := <-ch
    fmt.Println("Получено значение:", value)
}

Каналы бывают:

  • Небуферизированные — синхронные, отправка блокируется до получения.
  • Буферизированные — асинхронные, отправка блокируется только когда буфер заполнен.
// Создание буферизированного канала на 3 элемента
bufferedCh := make(chan string, 3)
bufferedCh <- "A"
bufferedCh <- "B"
bufferedCh <- "C" // Пока не заполнен — не блокирует

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

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

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "Сообщение из канала 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "Сообщение из канала 2"
    }()

    select {
    case msg1 := <-ch1:
        fmt.Println("Получено:", msg1)
    case msg2 := <-ch2:
        fmt.Println("Получено:", msg2)
    case <-time.After(3 * time.Second):
        fmt.Println("Таймаут — ни один канал не ответил")
    }
}

Оператор select часто используется для реализации таймаутов, неблокирующих операций и паттернов вроде «fan-in» (объединение нескольких каналов в один).

Практический пример: параллельная обработка данных

Давайте объединим всё вместе и напишем программу, которая параллельно обрабатывает список URL и собирает результаты:

package main

import (
    "fmt"
    "net/http"
    "sync"
    "time"
)

func fetchURL(url string, wg *sync.WaitGroup, results chan<- string) {
    defer wg.Done()
    start := time.Now()

    resp, err := http.Get(url)
    if err != nil {
        results <- fmt.Sprintf("Ошибка при запросе %s: %v", url, err)
        return
    }
    defer resp.Body.Close()

    elapsed := time.Since(start)
    results <- fmt.Sprintf("%s — статус %d, время: %v", url, resp.StatusCode, elapsed)
}

func main() {
    urls := []string{
        "https://google.com",
        "https://github.com",
        "https://golang.org",
    }

    var wg sync.WaitGroup
    results := make(chan string, len(urls)) // Буферизированный канал

    for _, url := range urls {
        wg.Add(1)                

Блоги

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