├── types.go ├── go.mod ├── pool_test.go ├── doc.go ├── batch.go ├── go.sum ├── LICENSE ├── range.go ├── pool.go ├── batch_test.go ├── parallel.go ├── parallel_test.go ├── range_test.go └── README.md /types.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | type intType interface { 4 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | 5 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr 6 | } 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dreamsofcode-io/loop 2 | 3 | go 1.22.1 4 | 5 | require github.com/stretchr/testify v1.9.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /pool_test.go: -------------------------------------------------------------------------------- 1 | package loop_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/dreamsofcode-io/loop" 10 | ) 11 | 12 | func TestPool(t *testing.T) { 13 | xs := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 14 | 15 | res := make([]int, 0, 3) 16 | 17 | for _, x := range loop.Pool(xs, 3) { 18 | res = append(res, x) 19 | time.Sleep(time.Millisecond) 20 | break 21 | } 22 | 23 | assert.Len(t, res, 3) 24 | } 25 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package loop provides a number of commonly used 2 | // methods for ranging over functions. 3 | // 4 | // Due to the experimental nature of the range-over-function feature 5 | // then this package will only work with go1.22 with the experimental 6 | // feature GOEXPERIMENT=rangefunc env var set 7 | // 8 | // ```go 9 | // GOEXPERIMENT=rangefunc go build 10 | // ``` 11 | // 12 | // When this feature is no longer experiemental, then this 13 | // package will likely be much more useful. 14 | package loop 15 | -------------------------------------------------------------------------------- /batch.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | // Batch is used to turn any slice into an iterator of batches, with the size 4 | // of each batch being the second parameter. 5 | // 6 | // This is useful if you want to perform batch operations on a large slice 7 | // of elements, for example, breaking up a large request into multiple 8 | // smaller ones. 9 | func Batch[E any](xs []E, size uint) func(func(int, []E) bool) { 10 | return func(yield func(int, []E) bool) { 11 | if size == 0 { 12 | return 13 | } 14 | 15 | index := 0 16 | 17 | for i := uint(0); i < uint(len(xs)); i += size { 18 | top := min(uint(len(xs)), uint(i)+size) 19 | batch := xs[i:top] 20 | 21 | if !yield(index, batch) { 22 | return 23 | } 24 | 25 | index += 1 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Elliott Minns 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /range.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | // Range creates a function iterator to iterate between two given 4 | // integer like values. 5 | // 6 | // The first argument is the starting value, which is included in the 7 | // iteration. The second argument is the stop value, which is when the 8 | // iteration is stopped. This value is not included. 9 | // 10 | // This function is basically loop.RangeWithStep(start, stop, 1) 11 | func Range[Int intType](start Int, stop Int) func(func(Int) bool) { 12 | return RangeWithStep(start, stop, 1) 13 | } 14 | 15 | // RangeWithStep creates a function iterator to iterate between two values 16 | // with a given step incrementor. 17 | // 18 | // The first value is always returned (provided the stop value is value for 19 | // the step amount) 20 | // The stop value is not included. 21 | // The step value can be either either greater than or less than 0. If the 22 | // step is 0 then no iteration will take place. 23 | func RangeWithStep[Int intType](start Int, stop Int, step Int) func(func(Int) bool) { 24 | return func(yield func(Int) bool) { 25 | if step == 0 { 26 | return 27 | } 28 | 29 | delta := int64(stop) - int64(start) 30 | steps := int64(delta) / int64(step) 31 | rem := int64(delta) % int64(step) 32 | if rem > 0 { 33 | steps += 1 34 | } 35 | 36 | for i := int64(0); i < steps; i++ { 37 | num := i*int64(step) + int64(start) 38 | if !yield(Int(num)) { 39 | return 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // Pool is used to perform bounded concurrency when iterating over the 9 | // elements in a slice. 10 | // 11 | // The workers parameter specifies the size of the concurrency pool 12 | // for iteration. For example, if a factor of 2 is given, then there 13 | // will only even be 2 iterations running at once. 14 | // 1 would effectively be a serial iteration. 15 | // 16 | // Bounded concurrency is useful in cases where the user may wish 17 | // to perform concurrency but in a reduced rate, so as to avoid 18 | // rate limits or running out of file descriptors. 19 | func Pool[E any](xs []E, workers int) func(func(int, E) bool) { 20 | return func(yield func(int, E) bool) { 21 | type iteration struct { 22 | val E 23 | i int 24 | } 25 | 26 | ctx, cancel := context.WithCancel(context.Background()) 27 | defer cancel() 28 | 29 | ch := make(chan iteration, workers) 30 | 31 | var wg sync.WaitGroup 32 | wg.Add(workers) 33 | 34 | for range workers { 35 | go func() { 36 | defer wg.Done() 37 | 38 | for x := range ch { 39 | select { 40 | case <-ctx.Done(): 41 | return 42 | default: 43 | if !yield(x.i, x.val) { 44 | cancel() 45 | } 46 | } 47 | } 48 | }() 49 | } 50 | 51 | for i, x := range xs { 52 | select { 53 | case <-ctx.Done(): 54 | return 55 | default: 56 | ch <- iteration{ 57 | val: x, 58 | i: i, 59 | } 60 | } 61 | } 62 | 63 | close(ch) 64 | 65 | wg.Wait() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /batch_test.go: -------------------------------------------------------------------------------- 1 | package loop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/dreamsofcode-io/loop" 9 | ) 10 | 11 | func TestBatchIncrementor(t *testing.T) { 12 | expected := 0 13 | 14 | for i, _ := range loop.Batch([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}, 2) { 15 | assert.Equal(t, expected, i) 16 | expected += 1 17 | } 18 | } 19 | 20 | func TestBatch(t *testing.T) { 21 | type input struct { 22 | items []int 23 | size uint 24 | } 25 | 26 | testCases := []struct { 27 | name string 28 | input input 29 | wants [][]int 30 | }{ 31 | { 32 | name: "happy path", 33 | input: input{ 34 | items: []int{ 35 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 36 | }, 37 | size: 3, 38 | }, 39 | wants: [][]int{ 40 | {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10}, 41 | }, 42 | }, 43 | { 44 | name: "batch size of 1", 45 | input: input{ 46 | items: []int{ 47 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 48 | }, 49 | size: 1, 50 | }, 51 | wants: [][]int{ 52 | {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, 53 | }, 54 | }, 55 | { 56 | name: "batch size of 0", 57 | input: input{ 58 | items: []int{ 59 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 60 | }, 61 | size: 0, 62 | }, 63 | wants: [][]int{}, 64 | }, 65 | } 66 | 67 | for _, tc := range testCases { 68 | t.Run(tc.name, func(t *testing.T) { 69 | res := [][]int{} 70 | for _, x := range loop.Batch(tc.input.items, tc.input.size) { 71 | res = append(res, x) 72 | } 73 | 74 | assert.Equal(t, res, tc.wants) 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /parallel.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // Parallel provides the ability to range over a slice concurrently. 9 | // Each element of the slice will be called within it's own goroutine. 10 | // 11 | // This function should not be used in hopes to speed up any pure compute 12 | // operation as there is an associated cost with spawning a new goroutine. 13 | // Instead, it makes sense if there are any long running tasks inside of 14 | // your loop. 15 | // 16 | // The benchmarks in parallel_test.go show a good example of when this 17 | // method will speed up performance. (using time.Sleep) 18 | func Parallel[E any](xs []E) func(func(int, E) bool) { 19 | return func(yield func(int, E) bool) { 20 | for i := range ParallelTimes(len(xs)) { 21 | x := xs[i] 22 | if !yield(i, x) { 23 | break 24 | } 25 | } 26 | } 27 | } 28 | 29 | // ParallelTimes allows you to peform a parallel iteration for a given 30 | // integer type. 31 | // 32 | // This is very similar to the loop.Parallel method, except that instead 33 | // of looping over a slice of elements, it instead will range for 34 | // the number of given 35 | func ParallelTimes[Int intType](num Int) func(func(Int) bool) { 36 | return func(yield func(Int) bool) { 37 | ctx, cancel := context.WithCancel(context.Background()) 38 | defer cancel() 39 | 40 | var wg sync.WaitGroup 41 | wg.Add(int(num)) 42 | 43 | for i := range uint64(num) { 44 | go func() { 45 | defer wg.Done() 46 | 47 | select { 48 | case <-ctx.Done(): 49 | return 50 | default: 51 | if !yield(Int(i)) { 52 | cancel() 53 | return 54 | } 55 | } 56 | }() 57 | } 58 | 59 | wg.Wait() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /parallel_test.go: -------------------------------------------------------------------------------- 1 | package loop_test 2 | 3 | import ( 4 | "sync/atomic" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/dreamsofcode-io/loop" 11 | ) 12 | 13 | var count uint64 = 1000 14 | 15 | func TestParallelShouldNotPanic(t *testing.T) { 16 | xs := []int{} 17 | 18 | for range 10000 { 19 | xs = append(xs) 20 | } 21 | 22 | for i, _ := range loop.Parallel(xs) { 23 | if i > 300 { 24 | break 25 | } 26 | } 27 | } 28 | 29 | // Parallel 30 | func TestParallel(t *testing.T) { 31 | testCases := []struct { 32 | name string 33 | input []int 34 | wants int 35 | }{ 36 | { 37 | name: "", 38 | input: []int{1, 2, 3, 4, 5}, 39 | wants: 15, 40 | }, 41 | } 42 | 43 | for _, tc := range testCases { 44 | t.Run(tc.name, func(t *testing.T) { 45 | sum := 0 46 | for _, x := range loop.Parallel(tc.input) { 47 | sum += x 48 | } 49 | 50 | assert.Equal(t, sum, tc.wants) 51 | }) 52 | } 53 | } 54 | 55 | func TestParallelTimes(t *testing.T) { 56 | sum := atomic.Int64{} 57 | counter := atomic.Int64{} 58 | for i := range loop.ParallelTimes(int64(5)) { 59 | counter.Add(1) 60 | sum.Add(i) 61 | } 62 | 63 | assert.Equal(t, counter.Load(), int64(5)) 64 | assert.Equal(t, sum.Load(), int64(10)) 65 | } 66 | 67 | func BenchmarkParallel(b *testing.B) { 68 | xs := make([]uint64, 0, count) 69 | for i := range count { 70 | xs = append(xs, i) 71 | } 72 | 73 | for range b.N { 74 | for _, _ = range loop.Parallel(xs) { 75 | time.Sleep(time.Microsecond) 76 | } 77 | } 78 | } 79 | 80 | func BenchmarkParallelExisting(b *testing.B) { 81 | xs := make([]uint64, 0, count) 82 | for i := range count { 83 | xs = append(xs, i) 84 | } 85 | 86 | for range b.N { 87 | for range xs { 88 | time.Sleep(time.Microsecond) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /range_test.go: -------------------------------------------------------------------------------- 1 | package loop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/dreamsofcode-io/loop" 9 | ) 10 | 11 | func TestRange(t *testing.T) { 12 | type input struct { 13 | start int 14 | stop int 15 | } 16 | 17 | testCases := []struct { 18 | name string 19 | input input 20 | wants []int 21 | }{ 22 | { 23 | name: "happy path", 24 | input: input{ 25 | start: 5, 26 | stop: 10, 27 | }, 28 | wants: []int{5, 6, 7, 8, 9}, 29 | }, 30 | { 31 | name: "single item", 32 | input: input{ 33 | start: 0, 34 | stop: 0, 35 | }, 36 | wants: nil, 37 | }, 38 | { 39 | name: "reverse item", 40 | input: input{ 41 | start: 10, 42 | stop: 0, 43 | }, 44 | wants: nil, 45 | }, 46 | } 47 | 48 | for _, tc := range testCases { 49 | t.Run(tc.name, func(t *testing.T) { 50 | var res []int 51 | for i := range loop.Range(tc.input.start, tc.input.stop) { 52 | res = append(res, i) 53 | } 54 | 55 | assert.Equal(t, res, tc.wants) 56 | }) 57 | } 58 | } 59 | 60 | func TestRangeWithStep(t *testing.T) { 61 | type input struct { 62 | start int 63 | stop int 64 | step int 65 | } 66 | 67 | testCases := []struct { 68 | name string 69 | input input 70 | wants []int 71 | }{ 72 | { 73 | name: "happy path", 74 | input: input{ 75 | start: 0, 76 | stop: 10, 77 | step: 2, 78 | }, 79 | wants: []int{0, 2, 4, 6, 8}, 80 | }, 81 | { 82 | name: "happy path #2", 83 | input: input{ 84 | start: 10, 85 | stop: 21, 86 | step: 5, 87 | }, 88 | wants: []int{10, 15, 20}, 89 | }, 90 | { 91 | name: "single item", 92 | input: input{ 93 | start: 0, 94 | stop: 0, 95 | step: 10, 96 | }, 97 | wants: nil, 98 | }, 99 | { 100 | name: "reverse step", 101 | input: input{ 102 | start: 10, 103 | stop: 0, 104 | step: -2, 105 | }, 106 | wants: []int{10, 8, 6, 4, 2}, 107 | }, 108 | { 109 | name: "reverse step #2", 110 | input: input{ 111 | start: 12, 112 | stop: 0, 113 | step: -3, 114 | }, 115 | wants: []int{12, 9, 6, 3}, 116 | }, 117 | { 118 | name: "zero step", 119 | input: input{ 120 | start: 0, 121 | stop: 100, 122 | step: 0, 123 | }, 124 | wants: nil, 125 | }, 126 | } 127 | 128 | for _, tc := range testCases { 129 | t.Run(tc.name, func(t *testing.T) { 130 | var res []int 131 | for i := range loop.RangeWithStep(tc.input.start, tc.input.stop, tc.input.step) { 132 | res = append(res, i) 133 | } 134 | 135 | assert.Equal(t, res, tc.wants) 136 | }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ This package is no longer supported 2 | 3 | Most of the functionality in this package is now provided by the standard library 4 | or does not work due to changes made to the way concurrency is handled in the new feature. 5 | 6 | # Loop 7 | 8 | Loop is a package that provides commonly used functions 9 | for ranging. 10 | 11 | In any case, this package should not be used moving forward 12 | 13 | 14 | ## Requirements 15 | 16 | This package requires Go 1.22 with the `GOEXPERIMENT=rangefunc` env 17 | var enabled. 18 | 19 | ## Usage 20 | 21 | The package provides a number of different functions for ranging. 22 | 23 | ### Parallel 24 | 25 | ⚠️ Since the features official release in 1.23, this method will now panic. 26 | 27 | A commonly used pattern in Go is to iterate over a slice of elements in parallel with a 28 | wait group. 29 | 30 | The parallel iterator provides this functionality in an easy to use interface 31 | 32 | ```go 33 | package main 34 | 35 | import "github.com/dreamsofcode-io/loop" 36 | 37 | func main() { 38 | xs := []int{1,2,3,4,5} 39 | squares := make([]int{}, len(xs)) 40 | 41 | // Each iteration runs in a goroutine 42 | for i, x := range loop.Parallel(xs) { 43 | // Simulate a long running task 44 | time.Sleep(time.Second) 45 | squares[i] = x * x 46 | } 47 | 48 | fmt.Println(squares) // [2, 4, 9, 16, 25] 49 | } 50 | ``` 51 | 52 | The above task will run in parallel, which means the total operation will only take 1 second, 53 | instead of the 5 it would take otherwise. 54 | 55 | ⚠️ One thing to be aware of is that each iteration runs in a separate goroutine. Therefore 56 | you'll want to make sure you are performing thread safe operations. 57 | 58 | The parallel task won't speed up any compute heavy operations, in that case, you're better 59 | off using a normal loop. However, in the event of performing network requests or async 60 | tasks, then using loop.Parallel will improve performance. 61 | 62 | ```go 63 | import ( 64 | "slog" 65 | "net/http" 66 | 67 | "github.com/dreamsofcode-io/loop" 68 | ) 69 | 70 | func main() { 71 | colors := []string{"green", "yellow", "blue"} 72 | 73 | results := make([]*http.Response{}, len(colors)) 74 | for _, color := range loop.Parallel(colors) { 75 | _, err := http.Post("http://example.com/colors", "text/plain", strings.NewReader(color)) 76 | if err != nil { 77 | slog.Error("oops", slog.Any(err)) 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | ### Pool 84 | The pool function is very similar to `loop.Parallel`, however it allows to caller to set the 85 | concurrency amount with the second argument. 86 | 87 | This is useful in the event you want bounded concurrency. 88 | 89 | ```go 90 | package main 91 | 92 | import "github.com/dreamsofcode-io/loop" 93 | 94 | func main() { 95 | xs := []int{1,2,3,4,5} 96 | 97 | // Each iteration runs in a goroutine 98 | for _, x := range loop.Pool(xs, 2) { 99 | // Simulate a long running task 100 | time.Sleep(time.Second) 101 | } 102 | } 103 | ``` 104 | 105 | In the above example, only 2 elements will be performed at a time. 106 | 107 | ### Batch 108 | 109 | The Batch function provides the ability to range over elements in batches. The size of each batch 110 | is decided by the given size argument, in which a batch will either be the same size or less than. 111 | 112 | The `loop.Batch` method runs in a single goroutine 113 | 114 | ```go 115 | import "github.com/dreamsofcode-io/loop" 116 | 117 | func main() { 118 | nums := []int{1, 2, 3, 4, 5} 119 | 120 | for i, batch := range loop.Batch(nums, 2) { 121 | fmt.Println(i, batch) 122 | } 123 | } 124 | ``` 125 | 126 | The above code will print the following output: 127 | 128 | ``` 129 | 0 [1, 2] 130 | 1 [3, 4] 131 | 2 [5] 132 | ``` 133 | 134 | If a batch size of 0 is passed in, then no iterations of the loop are performed. This behavior 135 | may change instead to panic as it's effectively a divide by 0. 136 | 137 | ### Range 138 | 139 | The range function allows you to iterate over a range of integer types. 140 | 141 | ```go 142 | import "github.com/dreamsofcode-io/loop" 143 | 144 | func main() { 145 | for i := range loop.Range(0, 5) { 146 | fmt.Println(i) 147 | } 148 | } 149 | ``` 150 | 151 | The above code will print out 152 | 153 | ``` 154 | 0 155 | 1 156 | 2 157 | 3 158 | 4 159 | ``` 160 | 161 | The `loop.Range` method includes the starting value, but excludes the stop value. 162 | 163 | ### ParallelTimes 164 | 165 | This method allows to perform a parallel operation for a given number of times. 166 | 167 | For example 168 | 169 | ```go 170 | import "github.com/dreamsofcode-io/loop" 171 | 172 | func main() { 173 | for i := range loop.ParallelTimes(10) { 174 | time.Sleep(time.Second) 175 | } 176 | } 177 | ``` 178 | 179 | will cause time.Sleep to be called 10 times in parallel. 180 | --------------------------------------------------------------------------------