Goroutines are lightweight threads managed by the Go runtime. They enable concurrent execution of functions, making it easy to write programs that do multiple things at once. Goroutines are one of Go's most powerful features.
💡 Key Points
- Goroutines are lightweight - thousands can run simultaneously
- Start a goroutine with the
go keyword - Goroutines run in the same address space
- The main function runs in its own goroutine
- Program exits when main goroutine completes
- Use channels or sync primitives for coordination
- Goroutines are multiplexed onto OS threads
- Much cheaper than OS threads (few KB of stack)
Starting Goroutines
Use the go keyword to run a function concurrently:
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
// Start goroutine
go say("world")
// Main goroutine continues
say("hello")
// Give goroutines time to finish
time.Sleep(500 * time.Millisecond)
fmt.Println("done")
}
Output:hello
world
hello
world
hello
world
done
Goroutines with Anonymous Functions
Commonly use anonymous functions with goroutines:
package main
import (
"fmt"
"time"
)
func main() {
// Launch multiple goroutines
for i := 0; i < 3; i++ {
go func(id int) {
fmt.Printf("Goroutine %d starting\n", id)
time.Sleep(time.Millisecond * 100)
fmt.Printf("Goroutine %d done\n", id)
}(i) // Pass i as argument to avoid closure issues
}
// Wait for goroutines to finish
time.Sleep(time.Second)
fmt.Println("All done")
}
Output:Goroutine 0 starting
Goroutine 1 starting
Goroutine 2 starting
Goroutine 0 done
Goroutine 1 done
Goroutine 2 done
All done
WaitGroup for Synchronization
Use sync.WaitGroup to wait for goroutines to complete:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrement counter when done
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Millisecond * 100)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
// Start 5 workers
for i := 1; i <= 5; i++ {
wg.Add(1) // Increment counter
go worker(i, &wg)
}
// Wait for all goroutines to finish
wg.Wait()
fmt.Println("All workers completed")
}
Output:Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done
All workers completed
Goroutine Syntax
| Pattern | Example | Description |
| Start goroutine | go function() | Run function concurrently |
| Anonymous function | go func() { ... }() | Inline goroutine |
| With arguments | go func(x int) { ... }(42) | Pass values to goroutine |
| Method call | go obj.Method() | Run method concurrently |
Synchronization Primitives
| Type | Purpose | Use Case |
sync.WaitGroup | Wait for collection of goroutines | Parallel task completion |
sync.Mutex | Mutual exclusion lock | Protect shared data |
sync.RWMutex | Reader/writer lock | Multiple readers, single writer |
sync.Once | Execute action exactly once | Initialization, singleton |
sync.Cond | Condition variable | Wait for/signal events |
| Channels | Communication between goroutines | Data exchange, synchronization |
Goroutine Characteristics
| Feature | Description | Details |
| Lightweight | Much cheaper than OS threads | Start with ~2KB stack, grows as needed |
| Multiplexed | Many goroutines on few OS threads | Go runtime manages scheduling |
| Fast startup | Creation is very fast | Faster than thread creation |
| Shared memory | Access same address space | Need synchronization for safety |
| Non-deterministic | Execution order not guaranteed | Use channels for coordination |
| Automatic cleanup | Runtime handles lifecycle | No manual thread management |
Common Patterns
| Pattern | Code | Use Case |
| Fire and forget | go function() | Background task, no result needed |
| Worker pool | for i := 0; i < N; i++ { go worker() } | Parallel processing |
| Fan-out | One input, many workers | Distribute work |
| Fan-in | Many inputs, one output | Collect results |
| Pipeline | Chain of processing stages | Data transformation |
| Timeout | select with timer | Bound goroutine execution time |
Best Practices
| Practice | Description | Reason |
| Pass data by value | Use function parameters | Avoid closure variable issues |
| Use WaitGroup | Coordinate goroutine completion | Better than time.Sleep |
| Avoid goroutine leaks | Ensure goroutines can exit | Prevent resource exhaustion |
| Use channels for communication | Share by communicating | Safer than shared memory |
| Limit concurrency | Don't spawn unlimited goroutines | Use worker pools |
| Handle errors properly | Return errors via channels | Don't panic in goroutines |
| Use context for cancellation | Pass context.Context | Graceful shutdown |
Common Pitfalls
| Pitfall | Problem | Solution |
| Closure variable capture | Loop variable shared | Pass as parameter: go func(i int){...}(i) |
| Goroutine leak | Goroutines never exit | Use context or done channels |
| Race conditions | Unsynchronized data access | Use mutexes or channels |
| Early program exit | Main exits before goroutines | Use WaitGroup or channels |
| Unbounded creation | Too many goroutines | Use worker pools, semaphores |
| Panic in goroutine | Crashes entire program | Use defer/recover |