What Are Goroutines?
A goroutine is a lightweight thread managed by the Go runtime. While OS threads use 1-2MB of stack memory, goroutines start with just 2KB. You can easily run millions of goroutines on a single machine. Start one with the go keyword:
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("Hello from %s (%d)\n", name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go sayHello("goroutine 1") // Starts concurrently
go sayHello("goroutine 2") // Starts concurrently
sayHello("main") // Runs in main goroutine
time.Sleep(time.Second) // Wait for goroutines (bad practice!)
}
Hello from main (0) Hello from goroutine 1 (0) Hello from goroutine 2 (0) Hello from main (1) Hello from goroutine 2 (1) Hello from goroutine 1 (1) ...
The output is interleaved — all three functions run concurrently. The order varies between runs.
sync.WaitGroup — Waiting for Goroutines
Using time.Sleep to wait is unreliable. Use sync.WaitGroup to wait for goroutines to finish:
import "sync"
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // Increment counter before starting goroutine
go func(id int) {
defer wg.Done() // Decrement when goroutine finishes
fmt.Printf("Worker %d done\n", id)
}(i) // Pass i as argument to avoid closure bug!
}
wg.Wait() // Block until counter reaches 0
fmt.Println("All workers done")
}
sync.WaitGroup or channels to synchronize goroutines. Never use time.Sleep. Call wg.Add() before launching the goroutine, and defer wg.Done() inside it.sync.Mutex — Protecting Shared State
When multiple goroutines access shared data, you need synchronization to prevent race conditions:
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.v[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}
⚠️ Common Mistake: Loop variable capture
// WRONG: all goroutines share the same i
for i := 0; i < 5; i++ {
go func() { fmt.Println(i) }() // Prints 5,5,5,5,5
}
// CORRECT: pass i as parameter
for i := 0; i < 5; i++ {
go func(id int) { fmt.Println(id) }(i) // Prints 0,1,2,3,4
}
2KB stack, Go scheduler
1MB stack, OS scheduler
No problem
System crash
Practice Exercises
Medium Build a Mini Project
Combine concepts from this tutorial to build a small utility or tool.
Medium Debug Challenge
Introduce a bug in one of the code examples and practice finding and fixing it.
Hard Refactoring Exercise
Rewrite one example using a different approach and compare the tradeoffs.