Understanding Goroutines and Channels in Go with Real Examples
Go (Golang) is well known for its simplicity and powerful concurrency model. Instead of relying on complex threading systems like many other languages, Go gives you goroutines and channels—lightweight tools that make concurrent programming easier to write and reason about.
In this article, we’ll explore:
- Why concurrency matters
- Goroutines vs OS threads
- Channels and communication
- A real-world worker pool implementation
1. Why Concurrency Matters
Modern applications are rarely doing just one thing at a time. A backend service might:
- Handle multiple HTTP requests simultaneously
- Call external APIs
- Read/write to a database
- Process background jobs
- Stream data
If all of this were done sequentially, performance would collapse.
Example problem (without concurrency)
Imagine processing 100 tasks, each taking 100ms:
100 × 100ms = 10,000ms (10 seconds)
That’s slow for real-world APIs.
With concurrency, many of these tasks can run at the same time, dramatically reducing total execution time.
2. Goroutines vs Threads
What is a goroutine?
A goroutine is a lightweight function managed by the Go runtime.
You create one using:
go doSomething()
Goroutines vs OS Threads
| Feature | Goroutines | OS Threads |
|---|---|---|
| Memory | ~2 KB initial stack | ~1 MB stack |
| Managed by | Go runtime | Operating system |
| Creation cost | Very cheap | Expensive |
| Scalability | Thousands/millions | Limited |
Example: Goroutines in action
package main
import (
"fmt"
"time"
)
func task(id int) {
fmt.Println("Starting task", id)
time.Sleep(1 * time.Second)
fmt.Println("Finished task", id)
}
func main() {
for i := 1; i <= 5; i++ {
go task(i)
}
time.Sleep(2 * time.Second)
}
What happens here?
- All tasks start almost instantly
- They run concurrently
- Main function waits (hack using
Sleep)
But this is not safe or clean—this is where channels come in.
3. Channels: Communication Between Goroutines
Go follows a principle:
“Do not communicate by sharing memory; instead, share memory by communicating.”
Channels allow goroutines to safely communicate.
Basic channel example
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "Hello from goroutine"
}()
msg := <-ch
fmt.Println(msg)
}
What’s happening?
- Goroutine sends data into channel
- Main function receives it
- They synchronize automatically
4. Worker Pool Pattern (Real Example)
One of the most important concurrency patterns in Go is the worker pool.
Problem
You have 100 tasks, but you don’t want:
- 100 goroutines (too many)
- Or 1 goroutine (too slow)
You want a fixed number of workers.
Solution: Worker Pool
We create:
- A job queue (channel)
- Workers (goroutines)
- A result channel
Worker Pool Implementation
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d started job %d\n", id, job)
time.Sleep(time.Second)
fmt.Printf("Worker %d finished job %d\n", id, job)
results <- job * 2
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Collect results
for r := 1; r <= 5; r++ {
fmt.Println("Result:", <-results)
}
}
What this achieves
- Only 3 goroutines handle all work
- Efficient CPU usage
- Controlled concurrency
- Safe communication using channels
5. Key Takeaways
Goroutines
- Lightweight concurrent functions
- Cheap to create
- Managed by Go runtime
Channels
- Safe communication between goroutines
- Prevent race conditions
- Enable synchronization
Worker Pools
- Control concurrency
- Improve performance
- Common in production systems
6. When to Use Concurrency
Use it when:
- Tasks are independent
- You are doing I/O (API calls, DB queries)
- You need parallel processing
Avoid it when:
- Task is too small (overhead > benefit)
- Shared state is too complex
Conclusion
Go’s concurrency model is one of its strongest features. With just goroutines and channels, you can build highly scalable systems without complex threading logic.
Once you master patterns like worker pools, pipelines, and fan-in/fan-out, you’ll be able to design backend systems that handle real-world load efficiently.