Для синхронизации горутин во время параллельного выполнения задач в Go принято использовать тип sync.WaitGroup, который позволяет определить группу горутин для совместного выполнения. При помощи sync.WaitGroup.Wait() можно установить блокировку, которая приостановит выполнение функции, пока не завершится вся группа горутин.

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

type Service struct {
    Name string
    Err  error
}

func main() {
    services := []Service{
        {
            Name: "service-1",
            Err:  nil,
        },
        {
            Name: "service-2",
            Err:  fmt.Errorf("service-2 error message"),
        },
    }

    notify(services)
}

func notify(services []Service) {
    wg := new(sync.WaitGroup)

    for _, service := range services {
        wg.Add(1)
        go func(s Service) error {
            fmt.Printf("Starting to notifing %s\n", s.Name)
            if s.Err != nil {
                wg.Done()
                return s.Err
            }
            time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
            fmt.Printf("Finished notifying %s\n", s.Name)

            wg.Done()
            return nil
        }(service)
    }

    wg.Wait()
    fmt.Println("All services notified!")
}
$ go run main.go
Starting to notifing service-2
Starting to notifing service-1
Finished notifying service-1
All services notified!

Как видно из примера 2 горутины запустились, но во время выполения одной возникла ошибка, которую не обработали.

Альтернативой sync.WaitGroup является тип errgroup.Group, который является частью пакета golang.org/x/sync/errgroup. Идея такая же, но errgroup.Group будет обрабатывать запуск каждой горутины и вернет ошибку, если одна из горутин вернет ее.

Изменим код выше для использования errgroup.Group:

func notify(services []Service) {
    g := new(errgroup.Group)

    for _, service := range services {
        s := service // https://golang.org/doc/faq#closures_and_goroutines
        g.Go(func() error {
            fmt.Printf("Starting to notifing %s\n", s.Name)
            if s.Err != nil {
                return s.Err
            }
            time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
            fmt.Printf("Finished notifying %s\n", s.Name)

            return nil
        })
    }

    err := g.Wait()
    if err != nil {
        fmt.Printf("Error notifying services: %v\n", err)
        return
    }

    fmt.Println("All services notified!")
}
$ go run main.go
Starting to notifing service-2
Starting to notifing service-1
Finished notifying service-1
Error notifying services: service-2 error message

Получившийся код длиннее оригинала, что связано с тем, что метод errorgroup.Group.Go() принимает функции, возвращающие ошибки, а метод errorgroup.Group.Wait() обрабатывает эти ошибки.

Стоит отметить, что в этом коде используется небольшая хитрость для копирования значения служебной переменной в локальную переменную внутри цикла for.

s := service

Это сделано, чтобы избежать распространенной ошибки с замыканиями , создаваемыми внутри цикла for. С этим типом ошибок лучше познакомится заранее, так как отладить код, когда впервые встретитесь с этой ошибкой, крайне сложно.

Также стоит отметить, что использование пакета errgroup может привести к отличающимся результатам, поскольку возвращается первая обнаруженная ошибка. Каждый запуск горутин не гарантирует порядок выполнения, что одна будет выполняться раньше другой. Об этом стоит помнить при отладке или тестировании кода.

Наконец, этот код на самом деле не вызывает go ... для запуска горутины. В этом нет необходимости с пакетом errgroup, потому что errorgroup.Group сделает это неявно.

Получившуюся версию код можно можно посмотреть на Go Playground .