
Go에서 고루틴을 병렬로 구현하고 실행할 때, 가장 먼저 배우는 도구는 sync.WaitGroup 일 것입니다.
하지만 Go에서는 병렬 구현이 쉬운만큼 반대로 어떻게 관리할 것인가를 많이 고민하게 되는 것 같습니다.
이번에는 이러한 관리 문제를 해결하기 위해 만들어진 errgroup에 대해서 함께 알아 봅시다.
1. errorgroup
sync.WaitGroup은 고루틴을 관리하는 좋은 도구였지만, 아쉬운 점이 있었습니다.
- 에러 전파 불가능 : 고루틴 내부에서 에러가 발생해도 상위 부모 루틴은 이를 알 수 있는 방법이 없습니다.
별도의 에러 채널을 직접 만들고 관리해야 합니다. - 리소스 낭비 : 예를 들어 10개의 고루틴을 병렬로 실행하고 있는데, 어떤 고루틴이 에러가 발생했더라도, 나머지 고루틴들은
그 사실을 알 수가 없음으로, 계속 자신의 동작을 수행하게 됩니다.
그래서 errgroup은 에러 핸들링과 작업 취소를 자동화하기 위해서 등장했습니다.
2. 핵심 매커니즘
errgroup 패키지의 핵심은 Go()와 Wait() 메서드에 있습니다.
- Go(f func() error) : 고루틴을 실행하며, 에러를 리턴하도록 되어 있어 상위로 에러를 전달할 수 있습니다.
- Wait() error : 모든 고루틴이 끝날 때까지 대기하다가, 실행되는 고루틴 중 가장 먼저 발생하는 에러를 리턴하며,
모두 성공하면 nil을 리턴합니다
3. 예제 코드
3-1 : 에러 발생 시 즉시 중단
여러 외부 서비스의 상태를 체크하는 시나리오이며 해당 예제 코드는
하나라도 실패하면 나머지 체크는 의미 없는 것을 가정한 코드입니다.
package main
import (
"context"
"fmt"
"net/http"
"time"
"golang.org/x/sync/errgroup"
)
type ServiceHealth struct {
Name string
URL string
}
func main() {
services := []ServiceHealth{
{"Google", "https://www.google.com"},
{"BrokenAPI", "https://this.url.does.not.exist.reo"},
{"Naver", "https://www.naver.com"},
{"GitHubAPI", "https://api.github.com"},
}
client := &http.Client{
Timeout: 4 * time.Second,
}
g, ctx := errgroup.WithContext(context.Background())
for _, svc := range services {
svc := svc
g.Go(func() error {
reqCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, "GET", svc.URL, nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "Go-Backend-Handbook-Example")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("%s 접속 실패: %w", svc.Name, err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("%s bad status: %s", svc.Name, resp.Status)
}
fmt.Printf("[%s] 상태: %s\n", svc.Name, resp.Status)
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Printf("system check error : %v\n", err)
return
}
fmt.Println("all service is good")
}
3-2 : 고루틴 실행 개수 제한
대량 작업을 처리할 때 시스템의 동시성을 직접 제어하는 방법에 대한 예제 입니다.
package main
import (
"context"
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(2)
for i := 1; i <= 5; i++ {
workerID := i
g.Go(func() error {
fmt.Printf("worker %d starting...\n", workerID)
select {
case <-time.After(1 * time.Second):
case <-ctx.Done():
fmt.Printf("worker %d canceled: %v\n", workerID, ctx.Err())
return nil
}
// force an error
if workerID == 3 {
return fmt.Errorf("worker %d failed", workerID)
}
fmt.Printf("worker %d finishing\n", workerID)
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Printf("error: %v\n", err)
return
}
fmt.Println("All tasks complete")
}
SetLimit(2)를 설정했는데, 5개의 작업을 넘기게 되더라도, 한 번에 2개씩만 실행이 되며,
한 작업이 끝나야 다음 작업이 큐에서 빠져나와 실행이 됩니다.
4. 전체 코드
https://github.com/reochoi109/go-handbook/blob/main/errGroup/basic/f1/main.go
go-handbook/errGroup/basic/f1/main.go at main · reochoi109/go-handbook
A personal handbook of Go patterns and best practices. Lightweight, practical code snippets for real-world backend development. - reochoi109/go-handbook
github.com
'프로그래밍 > golang' 카테고리의 다른 글
| [Golang] Context란? (0) | 2026.03.21 |
|---|---|
| [Golang] 채널(Channel)이란? (0) | 2026.03.18 |
| [Golang] Close() 에러를 무시하면 안 되는 이유 (0) | 2026.03.18 |
| [Golang] 에러 분류를 통한 Retry 구현 (0) | 2026.03.18 |
| [Golang] errors.Is와 As를 활용한 에러 핸들링 (0) | 2026.03.18 |