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

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

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

Начать курс

Асинхронное программирование в Go: от теории к практике

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

Что такое горутины (Goroutines)?

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

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

package main

import (
    "fmt"
    "time"
)

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

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

Обратите внимание: если не добавить time.Sleep, программа может завершиться до того, как горутина начнет выполняться. Для синхронизации лучше использовать другие механизмы, которые мы рассмотрим ниже.

Каналы (Channels): передача данных между горутинами

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

Рассмотрим пример с буферизированным и небуферизированным каналом:

package main

import "fmt"

func main() {
    // Небуферизированный канал (размер 0)
    ch := make(chan string)
    
    go func() {
        ch <- "Сообщение из горутины" // Отправляем данные
    }()
    
    msg := <-ch // Принимаем данные (блокируется, пока не придет сообщение)
    fmt.Println(msg)
    
    // Буферизированный канал (размер 2)
    bufferedCh := make(chan int, 2)
    bufferedCh <- 10
    bufferedCh <- 20
    
    fmt.Println(<-bufferedCh) // 10
    fmt.Println(<-bufferedCh) // 20
}

Важно: Небуферизированный канал блокирует отправителя до тех пор, пока получатель не примет данные. Буферизированный канал блокирует отправителя только когда буфер заполнен.

Select: мультиплексирование каналов

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

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    // Горутина 1: отправляет данные через 1 секунду
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "Данные из канала 1"
    }()
    
    // Горутина 2: отправляет данные через 2 секунды
    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(500 * time.Millisecond):
        fmt.Println("Таймаут! Ни один канал не ответил за 500 мс")
    }
}

В этом примере select сработает на таймауте, так как ни один канал не готов за 500 мс. Если бы мы убрали таймаут, программа бы ждала первый готовый канал.

Синхронизация с sync.WaitGroup

Когда нужно дождаться завершения группы горутин, на помощь приходит sync.WaitGroup. Он использует счетчик: Add(delta) увеличивает счетчик, Done() уменьшает, а Wait() блокирует выполнение до тех пор, пока счетчик не станет равен нулю.

package main

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

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

Практический пример: параллельный парсинг URL

Объединим все знания в реальном примере — параллельная загрузка нескольких веб-страниц с использованием горутин, каналов и sync.WaitGroup:

package main

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

func fetchURL(url string, ch chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        ch <- fmt.Sprintf("Ошибка при запросе %s: %v", url, err)
        return
    }
    defer resp.Body.Close()
    
    elapsed := time.Since(start)
    ch <- fmt.Sprintf("URL: %s | Статус: %d | Время: %v", url, resp.StatusCode, elapsed)
}

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

Блоги

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