Go errgroup에 인자 전달

박재훈·2024년 6월 21일
0

GO

목록 보기
20/23
post-custom-banner

WaitGroup

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의 방식과 맞지 않기도 하다.

또다른 방식으로는, 에러가 발생하면 wgDone을 잔뜩 호출해서 강제로 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

여기서 등장하는 게 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과 달리, errgroupfunc() 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에 인자를 전달할 수 있다고 한다.

Reference

profile
생각대로 되지 않을 때, 비로소 코딩은 재미있는 법.
post-custom-banner

0개의 댓글