Go에는 errgroup
이라는 게 있다. 간단하게 말하자면 WaitGroup
기능을 하면서 고루틴들 중 에러가 발생하면 정지하는 것이다.
예를 들어 슬라이스에 들어 있는 외부 API로부터 데이터를 비동기적으로 받아오는 로직을 작성한다고 해보자. WaitGroup
을 이용하면 보통 아래처럼 짤 것이다.
apis := []API{...}
var wg sync.WaitGroup
wg.Add(len(apis))
responses := make([]Res, len(apis))
errors := make([]error, len(apis))
for i, api := range apis {
go func(i int, api API) {
defer wg.Done()
res, err := api.Get(ctx)
if err != nil {
errors[i] = err
} else {
responses[i] = res
}
}(i, api)
}
wg.Wait()
이렇게 결과와 에러를 한데 모은 뒤 처리하는 생각을 보통 할 수 있겠다. 그런데 그룹 내에서 에러가 하나라도 발생했을 경우에 전체 그룹을 종료시키고 싶을 수 있다.
단순하게 생각해보면 panic
을 걸고 더 상위에서 recover
를 걸어서 recover
단에서 에러를 처리하는 방법을 생각해볼 수 있다. 하지만 Go에서는 웬만하면 panic
을 권장하지 않기도 하고, panic
-recover
방식은 어찌보면 다른 언어의 try-catch 방식이랑 비슷해서, 에러를 객체로 만들어 상부에 전달하는 Go의 방식과 맞지 않기도 하다.
또다른 방식으로는, 에러가 발생하면 wg
의 Done
을 잔뜩 호출해서 강제로 WaitGroup
을 중단시킨 뒤 해당 에러를 바깥으로 전달해서 처리하는 방식을 쓸 수도 있다. 아까보다는 Go스럽다고 할 수 있겠지만 많이 지저분하다. 이걸 구현하면 아래처럼 될 것이다.
var highErr *struct{
err error
idx int
}
for i, api := range apis {
go func(i int, api API) {
defer wg.Done()
res, err := api.Get(ctx)
if err != nil {
highErr.err = err
highErr.idx = i
for j := 0; j < len(apis); j++ {
wg.Done()
}
} else {
responses[i] = res
}
}(i, api)
}
wg.Wait()
여기서 등장하는 게 errgroup
이다. 비동기적 상황에서의 에러 처리를 효율적으로 할 수 있다. 코드를 보는 게 더 이해가 빠르다.
ctx, cancel := context.WithTimeout(time.Second * 10)
defer cancel()
eg, ectx := errgroup.WithContext(ctx)
eg.Go(func() error {
res1, err = api.Get(ectx)
return err
})
eg.Go(func() error {
res2, err = api.Get(ectx)
return err
})
그런데 고루틴이 실행될 함수를 마음대로 정의할 수 있었던 WaitGroup
과 달리, errgroup
은 func() error
타입의 함수만을 돌릴 수 있다. 그렇다면 인자를 어떻게 전달하냐는 문제가 발생하는데...
간단하게 스코프를 지정하는 것을 통해서 할 수가 있다고 한다.
예를 들어 for문에서 errgroup
을 실행한다고 해보자. for문을 도는 변수들을 인자로 넣고 싶다면 다음처럼 스코프 내에서 재정의 해주면 된다.
for i, value := range results {
i := i
value := value
eg.Go(func() error {
res, err = api.Get(ectx, i, value)
return err
})
}
또한 for문이 아닌 경우에는 중괄호({}
)로 묶어주면 된다.
value := fromSomeFunc()
{
value := value
eg.Go(func() error {
res, err = api.Get(ectx, value)
return err
})
}
아니면 익명함수를 만들어 거기에다가 전달하는 식으로 해도 된다.
value := fromSomeFunc()
func(value int) {
eg.Go(func() error {
res, err = api.Get(ectx, value)
return err
}
}(value)
이렇게 하면 클로저의 원리를 이용해서 errgroup
에 인자를 전달할 수 있다고 한다.