// Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package errgroup_test import ( "context" "errors" "fmt" "net/http" "os" "sync/atomic" "testing" "time" "golang.org/x/sync/errgroup" ) var ( Web = fakeSearch("web") Image = fakeSearch("image") Video = fakeSearch("video") ) type Result string type Search func(ctx context.Context, query string) (Result, error) func fakeSearch(kind string) Search { return func(_ context.Context, query string) (Result, error) { return Result(fmt.Sprintf("%s result for %q", kind, query)), nil } } // JustErrors illustrates the use of a Group in place of a sync.WaitGroup to // simplify goroutine counting and error handling. This example is derived from // the sync.WaitGroup example at https://golang.org/pkg/sync/#example_WaitGroup. func ExampleGroup_justErrors() { g := new(errgroup.Group) var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } for _, url := range urls { // Launch a goroutine to fetch the URL. url := url // https://golang.org/doc/faq#closures_and_goroutines g.Go(func() error { // Fetch the URL. resp, err := http.Get(url) if err == nil { resp.Body.Close() } return err }) } // Wait for all HTTP fetches to complete. if err := g.Wait(); err == nil { fmt.Println("Successfully fetched all URLs.") } } // Parallel illustrates the use of a Group for synchronizing a simple parallel // task: the "Google Search 2.0" function from // https://talks.golang.org/2012/concurrency.slide#46, augmented with a Context // and error-handling. func ExampleGroup_parallel() { Google := func(ctx context.Context, query string) ([]Result, error) { g, ctx := errgroup.WithContext(ctx) searches := []Search{Web, Image, Video} results := make([]Result, len(searches)) for i, search := range searches { i, search := i, search // https://golang.org/doc/faq#closures_and_goroutines g.Go(func() error { result, err := search(ctx, query) if err == nil { results[i] = result } return err }) } if err := g.Wait(); err != nil { return nil, err } return results, nil } results, err := Google(context.Background(), "golang") if err != nil { fmt.Fprintln(os.Stderr, err) return } for _, result := range results { fmt.Println(result) } // Output: // web result for "golang" // image result for "golang" // video result for "golang" } func TestZeroGroup(t *testing.T) { err1 := errors.New("errgroup_test: 1") err2 := errors.New("errgroup_test: 2") cases := []struct { errs []error }{ {errs: []error{}}, {errs: []error{nil}}, {errs: []error{err1}}, {errs: []error{err1, nil}}, {errs: []error{err1, nil, err2}}, } for _, tc := range cases { g := new(errgroup.Group) var firstErr error for i, err := range tc.errs { err := err g.Go(func() error { return err }) if firstErr == nil && err != nil { firstErr = err } if gErr := g.Wait(); gErr != firstErr { t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+ "g.Wait() = %v; want %v", g, tc.errs[:i+1], err, firstErr) } } } } func TestWithContext(t *testing.T) { errDoom := errors.New("group_test: doomed") cases := []struct { errs []error want error }{ {want: nil}, {errs: []error{nil}, want: nil}, {errs: []error{errDoom}, want: errDoom}, {errs: []error{errDoom, nil}, want: errDoom}, } for _, tc := range cases { g, ctx := errgroup.WithContext(context.Background()) for _, err := range tc.errs { err := err g.Go(func() error { return err }) } if err := g.Wait(); err != tc.want { t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+ "g.Wait() = %v; want %v", g, tc.errs, err, tc.want) } canceled := false select { case <-ctx.Done(): canceled = true default: } if !canceled { t.Errorf("after %T.Go(func() error { return err }) for err in %v\n"+ "ctx.Done() was not closed", g, tc.errs) } } } func TestTryGo(t *testing.T) { g := &errgroup.Group{} n := 42 g.SetLimit(42) ch := make(chan struct{}) fn := func() error { ch <- struct{}{} return nil } for i := 0; i < n; i++ { if !g.TryGo(fn) { t.Fatalf("TryGo should succeed but got fail at %d-th call.", i) } } if g.TryGo(fn) { t.Fatalf("TryGo is expected to fail but succeeded.") } go func() { for i := 0; i < n; i++ { <-ch } }() g.Wait() if !g.TryGo(fn) { t.Fatalf("TryGo should success but got fail after all goroutines.") } go func() { <-ch }() g.Wait() // Switch limit. g.SetLimit(1) if !g.TryGo(fn) { t.Fatalf("TryGo should success but got failed.") } if g.TryGo(fn) { t.Fatalf("TryGo should fail but succeeded.") } go func() { <-ch }() g.Wait() // Block all calls. g.SetLimit(0) for i := 0; i < 1<<10; i++ { if g.TryGo(fn) { t.Fatalf("TryGo should fail but got succeded.") } } g.Wait() } func TestGoLimit(t *testing.T) { const limit = 10 g := &errgroup.Group{} g.SetLimit(limit) var active int32 for i := 0; i <= 1<<10; i++ { g.Go(func() error { n := atomic.AddInt32(&active, 1) if n > limit { return fmt.Errorf("saw %d active goroutines; want ≤ %d", n, limit) } time.Sleep(1 * time.Microsecond) // Give other goroutines a chance to increment active. atomic.AddInt32(&active, -1) return nil }) } if err := g.Wait(); err != nil { t.Fatal(err) } } func BenchmarkGo(b *testing.B) { fn := func() {} g := &errgroup.Group{} b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { g.Go(func() error { fn(); return nil }) } g.Wait() }