Hello! Welcome to the fifth module of our course, "Essential Concurrency Patterns."
In the previous module, you mastered the art of building and distributing robust command-line tools. You now know how to create a complete application, from parsing arguments to cross-compiling and publishing it for different operating systems.
Now, we're going to unlock one of Go's most powerful features: concurrency. This will allow you to build tools and services that are significantly faster and more responsive. As a DevOps engineer, this is crucial for tasks like simultaneously monitoring multiple systems, running parallel automation jobs, or handling many incoming API requests.
This lesson kicks off our journey into concurrency. The learning outcome is to launch concurrent tasks using goroutines and understand their lightweight nature. We'll cover what goroutines are, how to create them, and why they are so efficient compared to traditional threads.
1. Concurrency vs. Parallelism: A Practical Distinction
Before we dive into Go's implementation, it's important to understand the difference between concurrency and parallelism. These terms are often used interchangeably, but they describe different concepts.
- Concurrency: Dealing with multiple tasks at once. The tasks are in progress over the same period, but they might not be executing at the exact same instant. Think of a chef chopping vegetables, then stirring a pot, then checking the oven—they are switching between tasks to keep everything moving forward.
- Parallelism: Doing multiple tasks at the exact same time. This requires multiple physical CPU cores. Think of two chefs, each chopping vegetables simultaneously at their own station.
Go's goroutines provide a simple way to write concurrent code. If you have multiple CPU cores, the Go runtime can execute your concurrent code in parallel, but the way you write the code remains the same.
This video from the sentdex channel gives a great practical analogy.
Goroutines - Go Lang Practical Programming Tutorial p.18
Watch this short clip to get a clear, practical explanation of concurrency versus parallelism.
Watch the segment from 00:14 to 01:25. Pay attention to the CEO analogy, which is a great way to understand the concept of 'dealing with' multiple things versus 'doing' multiple things.
2. Launching Your First Goroutine
In Go, a goroutine is an independently executing function that runs concurrently with other parts of your program. The syntax for starting one is remarkably simple: you just add the go keyword before a function call.
go myFunction() // This will now run concurrently
Let's see this in action. The same sentdex video provides a perfect hands-on demonstration.
Goroutines - Go Lang Practical Programming Tutorial p.18
This part of the video demonstrates how to convert a standard function call into a goroutine and highlights a critical behavior you need to be aware of.
Watch from 01:25 to 05:57. The key things to observe are: How the go keyword is used to launch the say() function as a goroutine. What happens when the main function finishes before the goroutine has a chance to complete its work. This is a fundamental concept in Go concurrency.
The Main Goroutine and Program Exit
As you saw in the video, a Go program exits as soon as the main function completes. The main function itself runs in a special goroutine, called the "main goroutine." When it finishes, all other goroutines are abruptly terminated, regardless of whether they have finished their work.

Consider this example, which you can run yourself:
package main
import (
"fmt"
"time"
)
func checkService(serviceName string) {
fmt.Printf("Checking status of %s...\n", serviceName)
time.Sleep(200 * time.Millisecond) // Simulate network latency
fmt.Printf("...%s is OK.\n", serviceName)
}
func main() {
go checkService("Database")
go checkService("API Gateway")
fmt.Println("Main function finished.")
}
If you run this code, you will likely only see "Main function finished." You won't see the output from checkService because the main function exits almost instantly, giving the two goroutines no time to run.
To see them run, we can add a time.Sleep to the end of main, just like in the video. This is a temporary hack for demonstration purposes only.
func main() {
go checkService("Database")
go checkService("API Gateway")
fmt.Println("Waiting for checks to complete...")
time.Sleep(500 * time.Millisecond) // Bad practice! For demo only.
fmt.Println("Main function finished.")
}
Now, you should see the output from both service checks. We've artificially kept the main goroutine alive long enough for the others to complete. In the next lesson, we'll learn the correct way to wait for goroutines using channels and wait groups.
Test your understanding!
What would be the most likely output of the code below, and why?
package main
import (
"fmt"
"time"
)
func main() {
go fmt.Println("Hello from the goroutine!")
time.Sleep(1 * time.Microsecond)
fmt.Println("Hello from main!")
}
Show answer
The most likely output is:
Hello from main!
Hello from the goroutine!
This is because launching a goroutine takes a small amount of time. The main function continues executing and will likely print its message and then sleep for a microsecond. In that time, the Go runtime will schedule and run the other goroutine, which will then print its message. If you remove the time.Sleep, the main function might exit before the goroutine gets a chance to run at all, and you would only see "Hello from main!".
3. The Lightweight Nature of Goroutines
You might be wondering, "Aren't these just threads?" Your experience with Python's threading module or OS-level threads in other contexts is relevant, but goroutines are fundamentally different and much more "lightweight."
An OS thread has a significant memory footprint (often 1MB or more of stack space) and context-switching between threads is relatively expensive because it's managed by the operating system kernel. Creating thousands of OS threads can quickly exhaust system resources.
Goroutines, on the other hand, are managed by the Go runtime, not the OS.
- Small Stack: They start with a very small stack (around 2KB) that can grow or shrink as needed.
- Runtime Scheduling: The Go runtime uses a scheduler to map many goroutines (N) onto a smaller number of OS threads (M). This is known as an M:N scheduler.
- Efficient Switching: Switching between goroutines is much faster than switching between OS threads because it happens in user space without needing a call to the OS kernel.
Because they are so cheap, it's common for a Go application to have thousands or even millions of active goroutines.
This article provides an excellent, concise summary of these characteristics.
OS Threads vs Goroutines: Understanding the Concurrency Model in Go
To solidify your understanding of what makes goroutines lightweight, please read the key characteristics section from the article "OS Threads vs Goroutines" by Damini Bansal.
In the article, find the section titled Goroutines. Read the list under the subsection Key Characteristics. Focus on points 1, 2, 3, and 6, which explain how they are managed, their low memory use, efficient switching, and scalability.
A Visual Analogy: The Go Kitchen
To make the relationship between goroutines, OS threads, and CPUs more intuitive, let's use an analogy.

The video below explains this M:N scheduling model using a clever analogy of gopher chefs, pans, and stoves.
Applied Go Quick Bits 007: Are goroutines threads?
This clip from the AppliedGo channel provides an excellent analogy to help you visualize how goroutines relate to OS threads and CPU cores.
Watch the segment from 00:19 to 04:29. Pay close attention to the analogy: Gopher Chef = Goroutine (the task) Pan = OS Thread (the execution context) Stove = CPU Core (the hardware that does the work) This will help you understand how the Go runtime efficiently manages many 'chefs' with a limited number of 'pans' and 'stoves'. The video also touches on how the scheduler handles blocking calls and balances work, a concept known as 'work-stealing'.
This model is what makes Go so powerful for building concurrent network services and tools—the exact kind of applications you'll be building. You can launch a goroutine for every incoming request or for every server you need to check, without worrying about the high overhead of traditional threads.
Conclusion
In this lesson, you've taken your first step into concurrent programming in Go. You've learned the fundamental concepts that set Go apart in building high-performance applications.
Key Takeaways:
- Goroutines are launched with the
gokeyword and execute functions concurrently. - The main program does not wait for goroutines to finish. If the
mainfunction exits, all other goroutines are terminated. - Goroutines are lightweight because they are managed by the Go runtime, have small, resizable stacks, and can be scheduled efficiently onto a small number of OS threads (the M:N scheduler).
- You can feasibly run thousands or millions of goroutines, making them ideal for tasks like handling concurrent network requests or parallelizing automation jobs.
Next Up
We've identified that using time.Sleep to wait for goroutines is an unreliable hack. To build robust concurrent programs, we need a reliable way to communicate with and synchronize our goroutines. In the next lesson, we will introduce channels, Go's primary mechanism for safe communication and synchronization between goroutines.