├── go.mod ├── .github └── workflows │ └── test.yml ├── go.sum ├── LICENSE ├── cmd └── example.go ├── parallel.go ├── README.md ├── .gitignore └── parallel_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smirzaei/parallel 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.0 // indirect 7 | github.com/pmezard/go-difflib v1.0.0 // indirect 8 | github.com/stretchr/objx v0.1.0 // indirect 9 | github.com/stretchr/testify v1.7.1 // indirect 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.18.x] 8 | os: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - uses: actions/setup-go@v3 12 | with: 13 | go-version: ${{ matrix.go-version }} 14 | - uses: actions/checkout@v3 15 | - run: go test ./... 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/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/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 8 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Soroush Mirzaei 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 | -------------------------------------------------------------------------------- /cmd/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/smirzaei/parallel" 8 | ) 9 | 10 | func main() { 11 | input := []int{1, 2, 3, 4, 5, 6, 7} 12 | 13 | parallel.ForEach(input, func(x int) { 14 | fmt.Printf("Processing %d\n", x) 15 | }) 16 | 17 | fmt.Println("=============================") 18 | output := parallel.Map(input, func(x int) int { 19 | fmt.Printf("Processing %d\n", x) 20 | return x * 2 21 | }) 22 | 23 | fmt.Printf("The final result is %v\n", output) 24 | 25 | fmt.Println("=============================") 26 | maxConcurrency := 2 27 | parallel.ForEachLimit(input, maxConcurrency, func(x int) { 28 | executionTime := time.Now().UTC().Format("15:04:05.999") 29 | fmt.Printf("%s - Processing %d\n", executionTime, x) 30 | 31 | time.Sleep(1 * time.Second) 32 | }) 33 | 34 | fmt.Println("=============================") 35 | output2 := parallel.MapLimit(input, maxConcurrency, func(x int) int { 36 | executionTime := time.Now().UTC().Format("15:04:05.999") 37 | fmt.Printf("%s - Processing %d\n", executionTime, x) 38 | time.Sleep(1 * time.Second) 39 | 40 | return x * 2 41 | }) 42 | 43 | fmt.Printf("The final result is %v\n", output2) 44 | } 45 | -------------------------------------------------------------------------------- /parallel.go: -------------------------------------------------------------------------------- 1 | package parallel 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | func ForEach[T any](arr []T, fn func(T)) { 8 | wg := &sync.WaitGroup{} 9 | wg.Add(len(arr)) 10 | 11 | for _, item := range arr { 12 | go func(x T) { 13 | defer wg.Done() 14 | 15 | fn(x) 16 | }(item) 17 | } 18 | 19 | wg.Wait() 20 | } 21 | 22 | func ForEachLimit[T any](arr []T, limit int, fn func(T)) { 23 | wg := &sync.WaitGroup{} 24 | wg.Add(len(arr)) 25 | 26 | limiter := make(chan struct{}, limit) 27 | 28 | for _, item := range arr { 29 | limiter <- struct{}{} 30 | go func(x T) { 31 | defer wg.Done() 32 | 33 | fn(x) 34 | <-limiter 35 | }(item) 36 | } 37 | 38 | wg.Wait() 39 | } 40 | 41 | func Map[T1 any, T2 any](arr []T1, fn func(T1) T2) []T2 { 42 | wg := &sync.WaitGroup{} 43 | wg.Add(len(arr)) 44 | 45 | output := make([]T2, len(arr), len(arr)) 46 | 47 | for i := range arr { 48 | go func(index int, x T1) { 49 | defer wg.Done() 50 | 51 | result := fn(x) 52 | output[index] = result 53 | 54 | }(i, arr[i]) 55 | } 56 | 57 | wg.Wait() 58 | return output 59 | } 60 | 61 | func MapLimit[T1 any, T2 any](arr []T1, limit int, fn func(T1) T2) []T2 { 62 | wg := &sync.WaitGroup{} 63 | wg.Add(len(arr)) 64 | 65 | output := make([]T2, len(arr), len(arr)) 66 | limiter := make(chan struct{}, limit) 67 | 68 | for i := range arr { 69 | limiter <- struct{}{} 70 | go func(index int, x T1) { 71 | defer wg.Done() 72 | 73 | result := fn(x) 74 | output[index] = result 75 | 76 | <-limiter 77 | }(i, arr[i]) 78 | } 79 | 80 | wg.Wait() 81 | return output 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Go Parallel 2 | === 3 | 4 | ![CI Status](https://github.com/smirzaei/parallel/actions/workflows/test.yml/badge.svg) 5 | 6 | Run Go loops in parallel. 7 | 8 | 9 | Installation 10 | --- 11 | 12 | Run 13 | 14 | ```BASH 15 | go get -u github.com/smirzaei/parallel 16 | ``` 17 | 18 | Examples 19 | --- 20 | 21 | ### ForEach 22 | 23 | Call the given function once for every element. Please note that the execution order is random. 24 | 25 | ```GO 26 | input := []int{1, 2, 3, 4, 5, 6} 27 | 28 | parallel.ForEach(input, func(x int) { 29 | fmt.Printf("Processing %d\n", x) 30 | }) 31 | 32 | // Output: 33 | 34 | // Processing 6 35 | // Processing 3 36 | // Processing 4 37 | // Processing 5 38 | // Processing 1 39 | // Processing 2 40 | ``` 41 | 42 | ### Map 43 | 44 | Call the given function once for every element and return a new slice with its results. Please note that the order of the elements of the output slice is the same as the input slice and it is preserved but the execution order is random. 45 | 46 | ```GO 47 | input := []int{1, 2, 3, 4, 5, 6} 48 | 49 | result := parallel.Map(input, func(x int) int { 50 | fmt.Printf("Processing %d\n", x) 51 | return x * 2 52 | }) 53 | 54 | fmt.Printf("The final result is %v\n", result) 55 | 56 | // Output: 57 | 58 | // Processing 6 59 | // Processing 1 60 | // Processing 2 61 | // Processing 3 62 | // Processing 4 63 | // Processing 5 64 | // The final result is [2 4 6 8 10 12] 65 | ``` 66 | 67 | ## Limiting the concurrency level 68 | 69 | If you need to limit the number parallel executions, you can use the following functions: 70 | 71 | ### ForEachLimit 72 | 73 | ```GO 74 | input := []int{1, 2, 3, 4, 5, 6, 7} 75 | 76 | maxConcurrency := 2 77 | parallel.ForEachLimit(input, maxConcurrency, func(x int) { 78 | executionTime := time.Now().UTC().Format("15:04:05.999") 79 | fmt.Printf("%s - Processing %d\n", executionTime, x) 80 | 81 | time.Sleep(1 * time.Second) 82 | }) 83 | 84 | // Output: 85 | 86 | // 09:47:54.071 - Processing 2 87 | // 09:47:54.071 - Processing 1 88 | // 09:47:55.071 - Processing 3 89 | // 09:47:55.071 - Processing 4 90 | // 09:47:56.071 - Processing 5 91 | // 09:47:56.071 - Processing 6 92 | // 09:47:57.071 - Processing 7 93 | ``` 94 | 95 | ### MapLimit 96 | 97 | ```GO 98 | input := []int{1, 2, 3, 4, 5, 6, 7} 99 | 100 | maxConcurrency := 2 101 | output := parallel.MapLimit(input, maxConcurrency, func(x int) int { 102 | executionTime := time.Now().UTC().Format("15:04:05.999") 103 | fmt.Printf("%s - Processing %d\n", executionTime, x) 104 | time.Sleep(1 * time.Second) 105 | 106 | return x * 2 107 | }) 108 | 109 | fmt.Printf("The final result is %v\n", output) 110 | 111 | // Output: 112 | 113 | // 09:53:58.998 - Processing 2 114 | // 09:53:58.998 - Processing 1 115 | // 09:53:59.998 - Processing 3 116 | // 09:53:59.998 - Processing 4 117 | // 09:54:00.999 - Processing 5 118 | // 09:54:00.999 - Processing 6 119 | // 09:54:01.999 - Processing 7 120 | // The final result is [2 4 6 8 10 12 14] 121 | ``` 122 | 123 | License 124 | === 125 | 126 | [MIT](https://github.com/smirzaei/parallel/blob/master/LICENSE) 127 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/go,macos,linux,windows,visualstudiocode,intellij+all 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,macos,linux,windows,visualstudiocode,intellij+all 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | ### Go Patch ### 22 | /vendor/ 23 | /Godeps/ 24 | 25 | ### Intellij+all ### 26 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 27 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 28 | 29 | # User-specific stuff 30 | .idea/**/workspace.xml 31 | .idea/**/tasks.xml 32 | .idea/**/usage.statistics.xml 33 | .idea/**/dictionaries 34 | .idea/**/shelf 35 | 36 | # AWS User-specific 37 | .idea/**/aws.xml 38 | 39 | # Generated files 40 | .idea/**/contentModel.xml 41 | 42 | # Sensitive or high-churn files 43 | .idea/**/dataSources/ 44 | .idea/**/dataSources.ids 45 | .idea/**/dataSources.local.xml 46 | .idea/**/sqlDataSources.xml 47 | .idea/**/dynamic.xml 48 | .idea/**/uiDesigner.xml 49 | .idea/**/dbnavigator.xml 50 | 51 | # Gradle 52 | .idea/**/gradle.xml 53 | .idea/**/libraries 54 | 55 | # Gradle and Maven with auto-import 56 | # When using Gradle or Maven with auto-import, you should exclude module files, 57 | # since they will be recreated, and may cause churn. Uncomment if using 58 | # auto-import. 59 | # .idea/artifacts 60 | # .idea/compiler.xml 61 | # .idea/jarRepositories.xml 62 | # .idea/modules.xml 63 | # .idea/*.iml 64 | # .idea/modules 65 | # *.iml 66 | # *.ipr 67 | 68 | # CMake 69 | cmake-build-*/ 70 | 71 | # Mongo Explorer plugin 72 | .idea/**/mongoSettings.xml 73 | 74 | # File-based project format 75 | *.iws 76 | 77 | # IntelliJ 78 | out/ 79 | 80 | # mpeltonen/sbt-idea plugin 81 | .idea_modules/ 82 | 83 | # JIRA plugin 84 | atlassian-ide-plugin.xml 85 | 86 | # Cursive Clojure plugin 87 | .idea/replstate.xml 88 | 89 | # Crashlytics plugin (for Android Studio and IntelliJ) 90 | com_crashlytics_export_strings.xml 91 | crashlytics.properties 92 | crashlytics-build.properties 93 | fabric.properties 94 | 95 | # Editor-based Rest Client 96 | .idea/httpRequests 97 | 98 | # Android studio 3.1+ serialized cache file 99 | .idea/caches/build_file_checksums.ser 100 | 101 | ### Intellij+all Patch ### 102 | # Ignores the whole .idea folder and all .iml files 103 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 104 | 105 | .idea/ 106 | 107 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 108 | 109 | *.iml 110 | modules.xml 111 | .idea/misc.xml 112 | *.ipr 113 | 114 | # Sonarlint plugin 115 | .idea/sonarlint 116 | 117 | ### Linux ### 118 | *~ 119 | 120 | # temporary files which can be created if a process still has a handle open of a deleted file 121 | .fuse_hidden* 122 | 123 | # KDE directory preferences 124 | .directory 125 | 126 | # Linux trash folder which might appear on any partition or disk 127 | .Trash-* 128 | 129 | # .nfs files are created when an open file is removed but is still being accessed 130 | .nfs* 131 | 132 | ### macOS ### 133 | # General 134 | .DS_Store 135 | .AppleDouble 136 | .LSOverride 137 | 138 | # Icon must end with two \r 139 | Icon 140 | 141 | # Thumbnails 142 | ._* 143 | 144 | # Files that might appear in the root of a volume 145 | .DocumentRevisions-V100 146 | .fseventsd 147 | .Spotlight-V100 148 | .TemporaryItems 149 | .Trashes 150 | .VolumeIcon.icns 151 | .com.apple.timemachine.donotpresent 152 | 153 | # Directories potentially created on remote AFP share 154 | .AppleDB 155 | .AppleDesktop 156 | Network Trash Folder 157 | Temporary Items 158 | .apdisk 159 | 160 | ### VisualStudioCode ### 161 | .vscode/* 162 | !.vscode/settings.json 163 | !.vscode/tasks.json 164 | .vscode/launch.json 165 | !.vscode/extensions.json 166 | *.code-workspace 167 | 168 | # Local History for Visual Studio Code 169 | .history/ 170 | 171 | ### VisualStudioCode Patch ### 172 | # Ignore all local history of files 173 | .history 174 | .ionide 175 | 176 | ### Windows ### 177 | # Windows thumbnail cache files 178 | Thumbs.db 179 | Thumbs.db:encryptable 180 | ehthumbs.db 181 | ehthumbs_vista.db 182 | 183 | # Dump file 184 | *.stackdump 185 | 186 | # Folder config file 187 | [Dd]esktop.ini 188 | 189 | # Recycle Bin used on file shares 190 | $RECYCLE.BIN/ 191 | 192 | # Windows Installer files 193 | *.cab 194 | *.msi 195 | *.msix 196 | *.msm 197 | *.msp 198 | 199 | # Windows shortcuts 200 | *.lnk 201 | 202 | # End of https://www.toptal.com/developers/gitignore/api/go,macos,linux,windows,visualstudiocode,intellij+all 203 | -------------------------------------------------------------------------------- /parallel_test.go: -------------------------------------------------------------------------------- 1 | package parallel 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestForEachToCallTheFunctionForEveryElement(t *testing.T) { 13 | // Arrange 14 | input := []int{1, 2, 3} 15 | expected := make([]int, 0, len(input)) 16 | 17 | sampleFunc := func(x int) { 18 | expected = append(expected, x) 19 | } 20 | 21 | // Act 22 | ForEach(input, sampleFunc) 23 | 24 | // Assert 25 | assert.ElementsMatch(t, input, expected) 26 | } 27 | 28 | func TestForEachToWorkFineConcurrently(t *testing.T) { 29 | // Arrange 30 | input1 := []int{1, 2, 3} 31 | expected1 := make([]int, 0, len(input1)) 32 | 33 | input2 := []int{4, 5, 6} 34 | expected2 := make([]int, 0, len(input2)) 35 | 36 | input3 := []int{7, 8, 9} 37 | expected3 := make([]int, 0, len(input3)) 38 | 39 | sampleFunc1 := func(x int) { 40 | expected1 = append(expected1, x) 41 | } 42 | 43 | sampleFunc2 := func(x int) { 44 | expected2 = append(expected2, x) 45 | } 46 | 47 | sampleFunc3 := func(x int) { 48 | expected3 = append(expected3, x) 49 | } 50 | 51 | // Act 52 | wg := &sync.WaitGroup{} 53 | wg.Add(3) 54 | go func() { 55 | ForEach(input2, sampleFunc2) 56 | wg.Done() 57 | }() 58 | 59 | go func() { 60 | ForEach(input1, sampleFunc1) 61 | wg.Done() 62 | }() 63 | 64 | go func() { 65 | ForEach(input3, sampleFunc3) 66 | wg.Done() 67 | }() 68 | 69 | wg.Wait() 70 | 71 | // Assert 72 | assert.ElementsMatch(t, input1, expected1) 73 | assert.ElementsMatch(t, input2, expected2) 74 | assert.ElementsMatch(t, input3, expected3) 75 | } 76 | 77 | func TestForEachLimitToCallTheFunctionForEveryElementAndNotExceedTheLimit(t *testing.T) { 78 | // Arrange 79 | input := []int{1, 2, 3, 4, 5} 80 | expected := make([]int, 0, len(input)) 81 | executionTime := make([]time.Time, 0, len(input)) 82 | concurrencyLimit := 3 83 | 84 | sampleFunc := func(x int) { 85 | executionTime = append(executionTime, time.Now().UTC()) 86 | expected = append(expected, x) 87 | time.Sleep(1 * time.Second) 88 | } 89 | 90 | // Act 91 | ForEachLimit(input, concurrencyLimit, sampleFunc) 92 | 93 | // Assert 94 | assert.ElementsMatch(t, input, expected) 95 | 96 | // Check each group has executing time close to one another 97 | checkTime := func(times []time.Time) { 98 | for i := 0; i < (len(times) - 1); i++ { 99 | t1 := times[i].Truncate(1 * time.Second) 100 | t2 := times[i+1].Truncate(1 * time.Second) 101 | 102 | assert.True(t, t1.Equal(t2)) 103 | } 104 | } 105 | 106 | group1 := executionTime[0:3] 107 | checkTime(group1) 108 | 109 | group2 := executionTime[3:5] 110 | checkTime(group2) 111 | 112 | // Check each group's execution is separated by 1 second (sleep duration time) 113 | firstGroupExecutionTime := group1[0].Truncate(1 * time.Second) 114 | secondGroupExecutionTime := group2[0].Truncate(1 * time.Second) 115 | 116 | assert.True(t, firstGroupExecutionTime.Before(secondGroupExecutionTime)) 117 | } 118 | 119 | func TestMapToCallTheFunctionForEveryElementAndReturnTheExpectedResult(t *testing.T) { 120 | // Arrange 121 | input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 122 | expected := []int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20} 123 | 124 | doubleFn := func(x int) int { 125 | return x * 2 126 | } 127 | 128 | // Act 129 | result := Map(input, doubleFn) 130 | 131 | // Assert 132 | assert.Equal(t, expected, result) 133 | } 134 | 135 | func TestMapToWorkFineConcurrently(t *testing.T) { 136 | // Arrange 137 | input := []int{1, 2, 3, 4, 5} 138 | expected1 := []int{2, 4, 6, 8, 10} 139 | expected2 := []int{3, 6, 9, 12, 15} 140 | expected3 := []int{1, 4, 9, 16, 25} 141 | expected4 := []int{1, 8, 27, 64, 125} 142 | 143 | doubleFn := func(x int) int { 144 | return x * 2 145 | } 146 | 147 | tripleFn := func(x int) int { 148 | return x * 3 149 | } 150 | 151 | squareFn := func(x int) int { 152 | return int(math.Pow(float64(x), 2)) 153 | } 154 | 155 | cubeFn := func(x int) int { 156 | return int(math.Pow(float64(x), 3)) 157 | } 158 | 159 | // Act & Assert 160 | wg := &sync.WaitGroup{} 161 | wg.Add(4) 162 | 163 | go func() { 164 | result := Map(input, doubleFn) 165 | assert.Equal(t, expected1, result) 166 | wg.Done() 167 | }() 168 | 169 | go func() { 170 | result := Map(input, tripleFn) 171 | assert.Equal(t, expected2, result) 172 | wg.Done() 173 | }() 174 | 175 | go func() { 176 | result := Map(input, squareFn) 177 | assert.Equal(t, expected3, result) 178 | wg.Done() 179 | }() 180 | 181 | go func() { 182 | result := Map(input, cubeFn) 183 | assert.Equal(t, expected4, result) 184 | wg.Done() 185 | }() 186 | 187 | wg.Wait() 188 | } 189 | 190 | func TestMapLimitToCallTheFunctionForEveryElementAndNotExceedTheLimit(t *testing.T) { 191 | // Arrange 192 | input := []int{1, 2, 3, 4, 5} 193 | expected := []int{2, 4, 6, 8, 10} 194 | executionTime := make([]time.Time, 0, len(input)) 195 | concurrencyLimit := 3 196 | 197 | doubleFn := func(x int) int { 198 | executionTime = append(executionTime, time.Now().UTC()) 199 | time.Sleep(1 * time.Second) 200 | 201 | return x * 2 202 | } 203 | 204 | // Act 205 | result := MapLimit(input, concurrencyLimit, doubleFn) 206 | 207 | // Assert 208 | assert.Equal(t, expected, result) 209 | 210 | // Check each group has executing time close to one another 211 | checkTime := func(times []time.Time) { 212 | for i := 0; i < (len(times) - 1); i++ { 213 | t1 := times[i].Truncate(1 * time.Second) 214 | t2 := times[i+1].Truncate(1 * time.Second) 215 | 216 | assert.True(t, t1.Equal(t2)) 217 | } 218 | } 219 | 220 | group1 := executionTime[0:3] 221 | checkTime(group1) 222 | 223 | group2 := executionTime[3:5] 224 | checkTime(group2) 225 | 226 | // Check each group's execution is separated by 1 second (sleep duration time) 227 | firstGroupExecutionTime := group1[0].Truncate(1 * time.Second) 228 | secondGroupExecutionTime := group2[0].Truncate(1 * time.Second) 229 | 230 | assert.True(t, firstGroupExecutionTime.Before(secondGroupExecutionTime)) 231 | } 232 | --------------------------------------------------------------------------------