← Назад к вопросам

Как понять, что все горутины завершились в Go?

3.0 Senior🔥 71 комментариев
#Конкурентность и горутины#Операционные системы и Linux

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Способы контроля завершения горутин в Go

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

1. Использование 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 * time.Duration(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("Все горутины завершены")
}

Ключевые моменты:

  • wg.Add(n) увеличивает счетчик на n
  • wg.Done() уменьшает счетчик на 1 (обычно используется с defer)
  • wg.Wait() блокирует выполнение до обнуления счетчика
  • Важно передавать WaitGroup по указателю, чтобы все горутины работали с одним экземпляром

2. Каналы для синхронизации

Каналы можно использовать как механизм сигнализации о завершении работы.

package main

import (
    "fmt"
    "time"
)

func worker(id int, done chan<- bool) {
    fmt.Printf("Воркер %d начал\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Воркер %d завершил\n", id)
    done <- true // Отправляем сигнал о завершении
}

func main() {
    done := make(chan bool, 3) // Буферизованный канал
    
    for i := 1; i <= 3; i++ {
        go worker(i, done)
    }
    
    // Ждем сигналы от всех горутин
    for i := 0; i < 3; i++ {
        <-done
    }
    
    fmt.Println("Все горутины завершены")
    close(done)
}

3. Контекст (Context) для управления жизненным циклом

Context особенно полезен при необходимости отмены операций или установки таймаутов.

package main

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

func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
    defer wg.Done()
    
    select {
    case <-time.After(time.Second * time.Duration(id)):
        fmt.Printf("Воркер %d завершил работу\n", id)
    case <-ctx.Done():
        fmt.Printf("Воркер %d отменен\n", id)
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    var wg sync.WaitGroup
    
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(ctx, i, &wg)
    }
    
    wg.Wait()
    fmt.Println("Основная горутина: все воркеры завершены или отменены")
}

4. ErrGroup для группировки горутин с обработкой ошибок

Пакет golang.org/x/sync/errgroup предоставляет расширенную функциональность для групп горутин.

package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
    "time"
)

func worker(ctx context.Context, id int) error {
    fmt.Printf("Воркер %d начал\n", id)
    time.Sleep(time.Second * time.Duration(id))
    
    if id == 2 {
        return fmt.Errorf("ошибка в воркере %d", id)
    }
    
    fmt.Printf("Воркер %d завершил\n", id)
    return nil
}

func main() {
    g, ctx := errgroup.WithContext(context.Background())
    
    for i := 1; i <= 3; i++ {
        workerID := i // Захват переменной для замыкания
        g.Go(func() error {
            return worker(ctx, workerID)
        })
    }
    
    // Wait ждет завершения всех горутин и возвращает первую ошибку
    if err := g.Wait(); err != nil {
        fmt.Printf("Завершено с ошибкой: %v\n", err)
    } else {
        fmt.Println("Все горутины успешно завершены")
    }
}

Рекомендации и лучшие практики

  1. Всегда используйте defer для wg.Done() — это гарантирует уменьшение счетчика даже при панике
  2. Добавляйте в WaitGroup перед запуском горутины — избегайте состояния гонки
  3. Для сложных сценариев комбинируйте подходы — например, Context + WaitGroup
  4. Избегайте sleep в продакшн-коде — используйте настоящие механизмы синхронизации
  5. Помните о deadlock — убедитесь, что каждая wg.Add() имеет соответствующую wg.Done()

Как понять, что горутины завершились в реальных приложениях?

В production-системах обычно используются:

  • Метрики и мониторинг (количество активных горутин через runtime.NumGoroutine())
  • Логирование ключевых этапов жизненного цикла
  • Graceful shutdown с использованием контекстов и каналов сигналов
  • Инструменты трассировки для отладки утечек горутин

Наиболее надежным и идиоматичным подходом является комбинация sync.WaitGroup для синхронизации завершения и context.Context для управления временем жизни и отмены операций. Эта комбинация покрывает большинство сценариев разработки на Go.

Как понять, что все горутины завершились в Go? | PrepBro