Concurrency in Go using Goroutine and WaitGroup
Go (or Golang) was created to address common drawbacks of other popular languages while maintaining their positive factors: fast compilation, efficient execution, and easy to write code. In today's world of multi-core processors and distributed systems, writing concurrent code is essential, yet traditionally it has been complex and error-prone. Go changes this trend of complexity.
Unlike other programming languages, where concurrency is achieved by using OS threads, Go provides an abstraction layer to automatically handle concurrency.
We assume, you have understanding of Go programming language. For demonstration purposes, we are going to build a simple ticket booking system. We will have two files: main.go and tickets.go. Here, main.go contains the main function and tickets.go contains functions related to ticket management. Let's create a Go project and the necessary files.
mkdir concurrency_demo
cd concurrency_demo
go mod init concurrency_demo
touch main.go tickets.go
Our tickets.go looks like this:
package main
import (
"fmt"
"strings"
"time"
)
var availableTickets uint = 100
type TicketInfo struct {
numOfTickets uint
userName string
}
func BookTicket() TicketInfo {
ticketInfo := TicketInfo{}
fmt.Println("Please enter data for options given below: ")
fmt.Print("Enter your first name [No space(s)]: ")
fmt.Scan(&ticketInfo.userName)
for {
fmt.Print("Enter number of tickets: ")
fmt.Scan(&ticketInfo.numOfTickets)
if ticketInfo.numOfTickets > 0 && ticketInfo.numOfTickets <= availableTickets {
availableTickets = availableTickets - ticketInfo.numOfTickets
break
} else {
continue
}
}
return ticketInfo
}
func SendTicket(ti TicketInfo) {
fmt.Println()
fmt.Println(strings.Repeat("#", 25))
fmt.Println("Generating pdf...")
time.Sleep(5 * time.Second)
fmt.Printf("Sent total number of %d tickets to %v.\n"+
"You can find ticket.pdf attached within.\n",
ti.numOfTickets, ti.userName)
fmt.Println(strings.Repeat("#", 25))
fmt.Println()
}
func DisplayExitMessage() {
fmt.Printf("Leftover tickets: %d.\n", availableTickets)
fmt.Println("Exiting program...")
}
We have created two functions BookTicket() that simulates ticket booking system and SendTicket() that mimics a ticket sending system that generates pdf and send tickets to users.
Without Concurrency
Our initial main.go runs everything sequentially (no concurrency involved):
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println("Welcome to ticket booking app")
fmt.Println(strings.Repeat("-", 35))
ticket := BookTicket()
SendTicket(ticket) // blocks for 5 seconds
DisplayExitMessage()
}
Run this program: go run .
In this program, we have to wait 5 seconds for the ticket to be sent. During that time the program freezes. Only after SendTicket() completes, DisplayExitMessage() runs.
Welcome to ticket booking app
-----------------------------------
Please enter data for options given below:
Enter your first name [No space(s)]: Alice
Enter number of tickets: 2
#########################
Generating pdf...
# ... 5 second wait ...
Sent total number of 2 tickets to Alice.
You can find ticket.pdf attached within.
#########################
Leftover tickets: 98.
Exiting program...
This is a problem. In a real-world application, its bad for users to wait for one slow operation. This is where Go's concurrency primitives shine.
With concurrency: Goroutine
A goroutine is a lightweight thread managed by the Go runtime, not the OS. You can spin up thousands of them with minimal overhead. Creating goroutine is very simple: just add the go keyword before a function call.
Let's fix the blocking problem in our ticket app:
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println("Welcome to ticket booking app")
fmt.Println(strings.Repeat("-", 35))
ticket := BookTicket()
go SendTicket(ticket) // now runs concurrently
DisplayExitMessage()
}
Run this and you'll notice something: DisplayExitMessage() now executes immediately, without waiting 5 seconds. The ticket sending happens in the background. But there's a problem in this code, the program exits before SendTicket ever finishes. We need a solution so that program waits for it. The solution is WaitGroup.
WaitGroup
The sync.WaitGroup is Go's built-in mechanism for waiting on a collection of goroutines to complete. Think of it as a counter: you increment it before launching a goroutine, and decrement it when that goroutine is done. The main goroutine blocks at wg.Wait() until the counter hits zero.
package main
import (
"fmt"
"strings"
"sync"
)
func main() {
fmt.Println("Welcome to ticket booking app")
fmt.Println(strings.Repeat("-", 35))
var wg sync.WaitGroup // WaitGroup variable
ticket := BookTicket()
wg.Add(1) // tell the WaitGroup to expect 1 goroutine
go func() {
defer wg.Done() // signal completion when the goroutine exits
SendTicket(ticket)
}() // function that is created and called
DisplayExitMessage() // runs immediately, doesn't block
wg.Wait() // block here until all goroutines are done
}
Now DisplayExitMessage() still runs immediately, but the program won't exit until SendTicket finishes. The output looks like:
Welcome to ticket booking app
-----------------------------------
Please enter data for options given below:
Enter your name: Alice
Enter number of tickets: 2
Leftover tickets: 98.
Exiting program...
#########################
Generating pdf...
... 5 second wait ...
Sent total number of 2 tickets to Alice.
You can find ticket.pdf attached within.
#########################
This is much closer to real-world behavior, the user gets instant feedback while background work continues.
Goroutine and WaitGroup used in combination, helps to run Go run concurrently. We also have another concept called Channels, that is related to concurrency in Go and used with Goroutine. To keep this article short and less boring, we'll cover Channels in different blog.
Leave a comment
Comments