Concurrency With Go
How to Write Concurrent Function Calls in Go
Go is an amazing language. I’d know, I just started using it this weekend. What are some of the cool things you can do with it? Well, how about writing concurrent function calls trivially! Let’s take a look.
Simple Sequential Function Calls
This code below is pretty simple. It just calls a function 20 times and prints i, from 0 -> 19
package main
import (
"fmt"
)
func print(i int) {
fmt.Println(i)
}
func main() {
for i := 0; i < 20; i++ {
print(i)
}
}
Output:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Simple Concurrent Function Calls
This code below attempts to fire off 20 concurrent function calls. We use the go
keyword before the function call to tell Go to run this in its own thread. Let’s see what happens if we run this code.
package main
import (
"fmt"
)
func print(i int) {
fmt.Println(i)
}
func main() {
for i := 0; i < 20; i++ {
go print(i)
}
}
Output:
That’s right: nothing was printed! Why did this happen? Well, the for
loop fires off 20 concurrent routines. Those routines are off running on their own. main()
then continues running. What is after the for
loop? Well, nothing. So main()
terminates. Those routines that split off never had a chance to even print anything! So how can we fix this? Let’s take a look at one approach using channels
.
Concurrent Function Calls with Channel
We can use a channel
in Go which is pretty much a semaphore. A channel is a buffer that can hold n
‘things’. For this case, we just want to add an int simply to signal something is inside. The type doesn’t matter for what we need.
package main
import (
"fmt"
)
func print(i int, c chan int) {
fmt.Println(i)
// this signals to the channel this routine is done
c <- 1
}
func main() {
num_calls := 20
c := make(chan int, num_calls)
for i := 0; i < num_calls; i++ {
go print(i, c)
}
// this loop won't terminate until 20 ints have been popped out
for i := 0; i < num_calls; i++ {
<- c
}
}
Output:
2 0 1 10 5 6 7 8 9 14 11 12 13 3 16 15 19 18 17 4
Now this looks concurrent!!
So, what happened can be boiled down to this. We set up a channel
of size 20. We loop 20 times and call 20 go routines. Next, main()
enters a for
loop. Each call is trying to pop out something from the channel. That for
loop won’t finish until 20 things have some out. If the buffer is empty, Go just waits around until something is added. What is adding things into the buffer? You guessed it! Our routines! So, each time a routine is done, it adds something to the channel, letting main()
inch closer to termination.
Concurrent Function Calls with WaitGroup
Here is another, more ‘Production Code Approved’ way of waiting for all go routines to finish running. It involves the use of a WaitGroup
.
package main
import (
"fmt"
"sync"
)
func print(i int, wg *sync.WaitGroup) {
// defer means run this line right before exiting the function
// wg.Done() signals to WaitGroup that this routine is done
defer wg.Done()
fmt.Println(i)
}
func main() {
wg := &sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go print(i, wg)
}
wg.Wait()
}
Output:
0 19 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18