Get started with Velocity
Join the Waitlist
Join Our Discord
Blogs

Goroutines, Channels and WaitGroups in Golang

Jeff Vincent
Jeff Vincent
  
December 2, 2022

Goroutines provide a means for multiprocessing in a Golang application, allowing multiple processes to run simultaneously. In addition, Channels and WaitGroups enable passing data between threads or blocking one thread until another completes. In this post, we explore examples of each.

Goroutines, Channels and WaitGroups in Golang

Goroutines are lightweight multiprocessing threads built into Go. They allow for concurrent operations to be executed in parallel and are defined with the go keyword when a given function is called, as shown below. When a function is called with the go keyword, Go automatically creates a separate thread to execute the defined process.

However, by default the main thread will run to completion regardless of whether any included goroutines complete. Let’s take a look at the following example to illustrate this:

package main

import "fmt"

func numbers() {
   for i := 0; i < 8; i++ {
       fmt.Println(i)
   }
}

func main() {
   fmt.Println("Main starting...")
   go numbers()
   fmt.Println("Main finished...")

}

Output:

Main starting...
Main finished...

When the same code is run without the go keyword, we get the following:

Main starting...
0
1
2
3
4
5
6
7
Main finished...

Why is this?

With the go keyword included, the numbers() function is executed in a separate thread; however, the main thread doesn’t wait for this second thread to complete. Rather, it just continues on to completion.

Channels

Channels allow goroutines to pass specific data types from one thread to another. Channels and the data type they can process must be defined before they can be used. For example, the following channel definition will accept an integer:

ch := make(chan int)

The syntax for passing data to a channel and processing data from a channel is as follows:

ch <- 10
a := <-ch
fmt.fmt.Println(a)  // 10

The data flows in the direction of the arrow, first into the channel ch and then out of ch into the variable a.

The following example illustrates how we can move data into the main thread with a channel:

package main

import (
   "fmt"
)

func numbers(ch chan int) {
   for i := 0; i < 8; i++ {
       ch <- i  // Here, we are passing the value `i` into the channel `ch`
   }
   close(ch)
}

func main() {
   ch := make(chan int)
   fmt.Println("Main starting...")
   go numbers(ch) // numbers() is running in a separate thread
   res := <-ch // Here, we are passing the data from `ch` to the variable `res`
   fmt.Println(res)
   fmt.Println("Main finished...")
}

Output:

Main starting...
0
Main finished...

Why is this?

By default, a channel can only manage a single value at a time, and until this value is removed from the channel, the thread is blocked. Because we are only removing one value from ch in the above code, only the first item returned from numbers() is printed.

To access all of the values the numbers() function is generating, we need to process the values as they are added to the channel, so that subsequent values can then be added in turn.

To illustrate this, we’ll add a for loop to our main() function that checks to see if there is a new value in the channel ch and if so, process it. Otherwise, the loop will end, and the main thread will continue to completion.

package main

import (
   "fmt"
)

func numbers(ch chan int) {
   for i := 0; i < 8; i++ {
       ch <- i
   }
   close(ch)
}

func main() {
   ch := make(chan int)
   fmt.Println("Main starting...")
   go numbers(ch)

   for {
       res, ok := <-ch
       if !ok {  // If there is nothing in the channel, the main thread continues
           break
       }
       fmt.Println(res) // Otherwise, it prints the value
   }
   fmt.Println("Main finished...")
}

WaitGroups

WaitGroups are another means of allowing additional threads to complete their process before the main thread runs to completion. They work by blocking the main thread until the goroutines associated with the WaitGroup have completed.

To illustrate this, we’ll create a WaitGroup in our main function and pass that to our goroutine:

package main

import (
   "fmt"
   "sync"
)

func numbers(wg *sync.WaitGroup) {
   defer wg.Done() // WaitGroup.Done() runs when the process has completed.
   for i := 0; i < 8; i++ {
       fmt.Println(i)
   }
}

func main() {
  fmt.Println("Main starting...")
  wg := new(sync.WaitGroup) // Create a waitgroup
  wg.Add(1) // The number of threads that must complete before unblocking the main thread
  go numbers(wg)
  wg.Wait()
  fmt.Println("Main finished...")
}

Output:

Main starting...
0
1
2
3
4
5
6
7
Main finished...

Although the output looks the same, the code is in fact behaving very differently here than it is above. In the above example, a channel is allowing the main thread to access data from a second thread as it runs, and it is the main thread that is printing the output.

Here though, the main thread is simply blocked until the second thread (created when the go numbers() function is called) competes its process. Once the second thread has processed to completion – i.e., printed all the numbers being generated – the main thread is free to continue.

Conclusion

Goroutines provide a means for multiprocessing in a Golang application. When the go keyword is added to a function call, a second thread is created in which the function’s process runs. This allows multiple processes to run simultaneously. By default, the main thread will continue to completion regardless of the state of any additional threads. However, Golang provides both Channels and WaitGroups in order to pass data from one thread to another, or simply block one thread until another completes. Above, we walked through examples of each approach.

Join the discussion!

Have any questions or comments about this post? Maybe you have a similar project or an extension to this one that you'd like to showcase? Join the Velocity Discord server to ask away, or just stop by to talk K8s development with the community.

Python class called ProcessVideo

Python class called ProcessVideo

Get started with Velocity