Goroutines in Go (In Depth)
Goroutines are the foundation of concurrency in Go. They allow functions to run independently and concurrently with very low overhead. In this lesson, you will deeply understand how goroutines work, how they are scheduled, and how to use them safely and effectively.
What Is a Goroutine?
A goroutine is a lightweight thread managed by the Go runtime. Unlike operating system threads, goroutines are cheap to create and consume very little memory.
A typical OS thread may require several megabytes of memory, while a goroutine starts with only a few kilobytes and grows as needed.
Why Goroutines Are Lightweight
- Managed by the Go runtime, not the OS
- Fast creation and destruction
- Efficient scheduling
- Scales to thousands or millions of tasks
This makes Go ideal for servers, APIs, and concurrent data processing.
Starting a Goroutine
You start a goroutine by prefixing a function call with the go keyword.
go someFunction()
Once started, the goroutine runs concurrently with the rest of the program.
Basic Goroutine Example
package main
import (
"fmt"
"time"
)
func greet(name string) {
fmt.Println("Hello,", name)
}
func main() {
go greet("Alice")
go greet("Bob")
time.Sleep(time.Second)
}
Here, two goroutines execute independently. The sleep ensures the main function does not exit early.
Understanding Program Exit
When the main function exits, all goroutines are immediately terminated.
This is why you must explicitly coordinate goroutines instead of relying on timing.
Goroutine Scheduling (Conceptual)
Go uses an internal scheduler based on three components:
- G – Goroutine
- M – OS Thread (Machine)
- P – Processor (logical execution context)
The runtime maps many goroutines onto a small number of OS threads, efficiently multiplexing execution.
Example with Loop and Goroutines
A common beginner mistake is launching goroutines inside loops incorrectly.
package main
import (
"fmt"
"time"
)
func main() {
for i := 1; i <= 3; i++ {
go func(n int) {
fmt.Println("Goroutine:", n)
}(i)
}
time.Sleep(time.Second)
}
Here, each goroutine correctly receives its own value of i.
Incorrect Loop Example (Do Not Do This)
for i := 1; i <= 3; i++ {
go func() {
fmt.Println(i)
}()
}
This can cause unexpected results because all goroutines may access the same variable.
Real-World Example: Processing Orders
Imagine processing multiple orders in an e-commerce system. Each order can be handled independently.
func processOrder(orderID int) {
fmt.Println("Processing order:", orderID)
}
func main() {
orders := []int{101, 102, 103, 104}
for _, id := range orders {
go processOrder(id)
}
time.Sleep(time.Second)
}
Each order is processed concurrently, improving performance and responsiveness.
When to Use Goroutines
- Handling multiple requests
- Background processing
- Parallel data processing
- I/O-bound tasks
When Not to Use Goroutines
- Very small, sequential tasks
- When shared state is not synchronized
- When concurrency adds unnecessary complexity
Key Takeaways
- Goroutines are lightweight and efficient
- They run concurrently, not sequentially
- Main function controls program lifetime
- Proper synchronization is required
Practice Exercises
Exercise 1
Create a program that launches five goroutines, each printing a unique number.
Exercise 2
Modify the program to process a slice of values concurrently.
What’s Next?
In the next lesson, you will learn about Channels — the core mechanism Go uses to communicate between goroutines safely.