waiteGroup vs errGroup

Divan·2023년 10월 22일
post-thumbnail

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

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

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을 하는데 있어 유리할것으로 생각합니다.

profile
하루 25분의 투자

1개의 댓글

comment-user-thumbnail
2023년 10월 22일

따봉따봉~!

답글 달기