Understanding Goroutines and Channels
Table of Contents
Using WaitGroup for Goroutines Synchronization
In Go, we need to wait for our goroutines to finish in our main so that we do not leak goroutines. We can do so using a sync.WaitGroup. It allows you to wait for a collection of goroutines to finish executing before proceeding. We will we add goroutines that we start by calling wg.add and at the end, we can wait for all these goroutines before the execution of main ends.
A WaitGroup works by maintaining a counter that tracks how many goroutines have been launched. You increment the counter using wg.Add(1) before starting each goroutine, and you decrement it using wg.Done() when a goroutine finishes. Finally, you block the main thread from exiting by calling wg.Wait(), which will wait for all goroutines to complete.
Defer gives us the opportunity to put some function that will be executed once the end of the scope of the function is met. So if there is an error while executing the goroutine, wg.Done() would still be called before we exit the function. Here, we have an example below demonstrating WaitGroup
1func main() {
2 var wg sync.WaitGroup
3 for _, salutation := range[] string{"hello", "greetings", "good day"} {
4 wg.Add(1);
5 go print(salutation, &wg);
6 }
7 wg.Wait()
8}
9 func print(salutation string, wg *sync.WaitGroup) {
10 defer wg.Done()
11 fmt.Println(salutation);
12}Synchronization using channels
In Go, channels are a powerful way to synchronize and communicate between goroutines. In this example, we demonstrate how to use channels to synchronize the execution of multiple goroutines while avoiding issues such as infinite loops when reading from a channel.
In this example, we have the main goroutine creating five other goroutines to write integers to an intstream channel. The main goroutine will then read the values from the channel. To avoid an infinite loop while reading from the channel, we need to ensure that the channel is properly closed once all the writing goroutines have finished their execution. We can achieve this by using a WaitGroup to wait for all goroutines to complete before closing the channel.
1func main() {
2 var wg sync.WaitGroup
3 intstream := make(chan int)
4 for i := 1; i <= 5; i++ {
5 wg.Add(1)
6 go write(&wg, intstream, i)
7 }
8 go closeChan(&wg, intstream)
9 for integer := range intstream {
10 fmt.Printf("%v ", integer)
11 }
12}
13
14func closeChan(wg *sync.WaitGroup, intstream chan int) {
15 wg.Wait()
16 close(intstream)
17}
18
19func write(wg *sync.WaitGroup, intstream chan int, num int ) {
20 defer wg.Done()
21 intstream <- num
22}Using done channels
In Go, a done channel can be used to notify goroutines when they should stop their work. In this example, the main goroutine is reading from a randStream channel, which is being written to by a separate writer goroutine. After reading three values from the channel, the main goroutine closes the done channel, signaling the writer to stop writing and exit gracefully.
The writer goroutine listens for a signal on the done channel. When it receives the signal (i.e., when the channel is closed), it stops writing to the randStream channel and terminates. This pattern is a useful way to control the flow of goroutines and ensure that they clean up after themselves.
1func main() {
2 done := make(chan interface{})
3 randStream := make(chan int)
4 go write(done, randStream)
5 for i := 1; i <= 3; i++ {
6 fmt.Printf("%d: %d
7", i, <-randStream)
8 }
9
10 close(done)
11}
12
13func write(done <-chan interface{}, randStream chan<- int) {
14 for {
15 select {
16 case randStream <- rand.Intn(100): // Send a random number to randStream
17 case <-done: // If done channel is closed, exit the loop
18 return
19 }
20 }
21}