go언어에서 병렬처리를 위해서 주로 chennel과 gorutine을 사용하게 되면서, gorutine을 적절히 handling하는 방법에 대해서 고민하게 된다, errgroup은 gorutine을 잘 handling하도록 도와준다.
여러개의 gorutine이 병렬로 실행 중 err가 발생한다면, errgroup 패키지를 활용하여 간단히 처리 가능하다.
예제에서는 int배열에서 특정수를 찾았을때 만약 없다면 err를 발생하게 하였습니다.
type Numbers []int
func (n *Numbers) Index(target int) (int, error) {
idx := slices.Index(*n, target)
if idx < 0 {
return idx, errors.New("can not find target")
}
return idx, nil
}
func (n *Numbers) AppendRandom() {
num := rand.Intn(10)
*n = append(*n, num)
}
waiteGroup을 이용하여 rutine안에서 err를 handling하기 위해서는 context를 생성하고 이를 gorutine에게 전달하여 handling 하여야 한다.
func TestWaitGroup(t *testing.T) {
numbers := make(Numbers, 0)
for i := 0; i < 10; i++ {
numbers.AppendRandom()
}
t.Log("numbers", numbers)
wg := sync.WaitGroup{}
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 10; i++ {
wg.Add(1)
go func(cancel context.CancelFunc) {
err := func(ctx context.Context) error {
defer wg.Done()
select {
case <-ctx.Done():
return ctx.Err()
default:
target := rand.Intn(10)
idx, err := numbers.Index(target)
if err != nil {
return err
}
t.Log("found idx", idx)
return nil
}
}(ctx)
if err != nil {
t.Error("error", err)
cancel()
}
}(cancel)
}
wg.Wait()
t.Log("done")
}
출력 결과물을 보면, conext가 종료되어 goruine이 종료 된것을 확인 가능하다.

errGroup을 사용한다면 아래와 같이 코드의 길이가 비교적 단단하고 명료하고, 신경써야 할것(context, waitGroup counter)들이 많이 줄어 든것 확인 가능하다.
[사용법]
1. errGroup 생성
2. Go()로 함수 등록
3. Wait()로 대기
func TestErrGroup(t *testing.T) {
numbers := make(Numbers, 0)
for i := 0; i < 10; i++ {
numbers.AppendRandom()
}
t.Log("numbers", numbers)
eg := errgroup.Group{}
for i := 0; i < 10; i++ {
routine := i
eg.Go(func() error {
target := rand.Intn(10)
idx, err := numbers.Index(target)
if err != nil {
return err
}
t.Log("routine", routine, "found idx", idx)
return nil
})
}
err := eg.Wait()
if err != nil {
t.Error("error", err)
}
t.Log("done")
}
출력 결과를 보면 다음과 같이 routine이 종료된것을 확인 가능하다.

그렇다면 방금사용하는 errGroup패키지는 어떤 방식으로 err를 handling하는지 확인 해보자.
x/sync/errgroup
type Group struct {
cancel func(error)
wg sync.WaitGroup
sem chan token
errOnce sync.Once
err error
}
func (g *Group) Wait() error {
g.wg.Wait()
if g.cancel != nil {
g.cancel(g.err)
}
return g.err
}
func (g *Group) Go(f func() error) {
if g.sem != nil {
g.sem <- token{}
}
g.wg.Add(1)
go func() {
defer g.done()
if err := f(); err != nil {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel(g.err)
}
})
}
}()
}
errgoup패키지에서도 wg라는 waitGroup을 인자로 가지고 있다, GO()를 할때 waitGroup의 count 수를 관리하고 있으며, Wait()에도 waiteGroup으 wg.Wait() 함수를 이용하여 대기한다.
특정경우가 아니라면 errGroup을 이용하여 grouptine을 제어하는것이 가독성 및 errhandling을 하는데 있어 유리할것으로 생각합니다.
따봉따봉~!