Goroutines are Go's lightweight threads that make concurrent programming easy and efficient. They're much cheaper than OS threads and enable you to run thousands of concurrent operations with minimal overhead.
What Are Goroutines?
Goroutines are functions running concurrently with other goroutines in the same address space.
Basic Syntax
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world") // Start goroutine
say("hello") // Run in main goroutine
}The go keyword starts a new goroutine.
Lightweight Nature
Goroutines are extremely lightweight:
func main() {
// Start 100,000 goroutines
for i := 0; i < 100000; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
time.Sleep(time.Second)
}This would be impossible with OS threads.
Goroutine Lifecycle
Understanding goroutine lifecycle is crucial for effective concurrency.
Starting Goroutines
func worker(id int) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
for i := 1; i <= 3; i++ {
go worker(i)
}
time.Sleep(2 * time.Second)
}Anonymous Goroutines
go func() {
fmt.Println("Anonymous goroutine")
}()Be careful with variable capture:
// WRONG: All goroutines capture same i
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // Prints 3, 3, 3
}()
}
// RIGHT: Pass i as parameter
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n) // Prints 0, 1, 2
}(i)
}WaitGroups
sync.WaitGroup waits for a collection of goroutines to finish.
Basic Usage
import "sync"
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait() // Wait for all workers
}Common Pattern
func process(items []int) {
var wg sync.WaitGroup
sem := make(chan struct{}, 10) // Limit to 10 concurrent
for _, item := range items {
wg.Add(1)
sem <- struct{}{} // Acquire
go func(i int) {
defer wg.Done()
defer func() { <-sem }() // Release
// Process item
fmt.Println(i)
}(item)
}
wg.Wait()
}Mutex for Synchronization
sync.Mutex protects shared data.
Basic Mutex
import (
"sync"
)
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}RWMutex
RWMutex allows multiple readers or one writer:
type SafeMap struct {
mu sync.RWMutex
data map[string]int
}
func (m *SafeMap) Get(key string) int {
m.mu.RLock()
defer m.mu.RUnlock()
return m.data[key]
}
func (m *SafeMap) Set(key string, value int) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = value
}Once for Single Execution
sync.Once ensures a function runs only once.
var once sync.Once
var instance *Database
func getDatabase() *Database {
once.Do(func() {
instance = connectToDatabase()
})
return instance
}Context for Cancellation
context package manages goroutine lifecycles.
Basic Cancellation
import "context"
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker cancelled")
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go worker(ctx)
time.Sleep(3 * time.Second)
}With Values
func handleRequest(ctx context.Context) {
userID := ctx.Value("userID").(string)
fmt.Println("Handling request for", userID)
}
func main() {
ctx := context.WithValue(context.Background(), "userID", "12345")
handleRequest(ctx)
}Atomic Operations
sync/atomic provides low-level atomic operations.
Basic Atomic Operations
import "sync/atomic"
var counter int64
func increment() {
atomic.AddInt64(&counter, 1)
}
func get() int64 {
return atomic.LoadInt64(&counter)
}Compare and Swap
var config int64
func updateConfig(newConfig int64) bool {
return atomic.CompareAndSwapInt64(&config, 0, newConfig)
}Pool for Object Reuse
sync.Pool reuses objects to reduce allocation.
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func getBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func putBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}Common Patterns
Worker Pool
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, j)
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// Start 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 a := 1; a <= 5; a++ {
<-results
}
}Fan-Out, Fan-In
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func fanIn(ch1, ch2 <-chan int) <-chan int {
out := make(chan int)
go func() {
for {
select {
case n := <-ch1:
out <- n
case n := <-ch2:
out <- n
}
}
}()
return out
}Pipeline
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// Set up pipeline
c := gen(2, 3)
out := sq(c)
// Consume output
fmt.Println(<-out) // 4
fmt.Println(<-out) // 9
}Rate Limiting
func main() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
bursts := make(chan time.Time, 5)
for i := 0; i < 5; i++ {
bursts <- time.Now()
}
go func() {
for t := range ticker.C {
bursts <- t
}
}()
requests := make(chan int, 5)
for i := 1; i <= 5; i++ {
requests <- i
}
close(requests)
for req := range requests {
<-bursts
fmt.Println("Request", req, time.Now())
}
}Best Practices
- 1Always use WaitGroup to wait for goroutines
- 2Pass context for cancellation
- 3Use mutex for shared data
- 4Be careful with goroutine leaks
- 5Limit concurrency with semaphores
- 6Handle panics in goroutines
- 7Use channels for communication
Common Mistakes
Goroutine Leaks
// WRONG: Goroutine never exits
func leak() {
ch := make(chan int)
go func() {
val := <-ch // Blocks forever
fmt.Println(val)
}()
}
// RIGHT: Provide way to exit
func noLeak() {
ch := make(chan int)
done := make(chan struct{})
go func() {
select {
case val := <-ch:
fmt.Println(val)
case <-done:
return
}
}()
close(done)
}Data Races
// WRONG: Data race
var counter int
go func() { counter++ }()
go func() { counter++ }()
// RIGHT: Use mutex
var mu sync.Mutex
var counter int
go func() {
mu.Lock()
counter++
mu.Unlock()
}()Closing Channels Multiple Times
// WRONG: Panic on second close
close(ch)
close(ch)
// RIGHT: Close once
defer once.Do(func() {
close(ch)
})Performance Considerations
Goroutines are cheap but not free:
// Be reasonable with concurrency
const maxConcurrency = 100
sem := make(chan struct{}, maxConcurrency)
for _, item := range items {
sem <- struct{}{}
go func(i Item) {
defer func() { <-sem }()
process(i)
}(item)
}Conclusion
Goroutines make concurrent programming in Go straightforward and efficient. Combined with channels and proper synchronization, they enable you to write highly concurrent applications that scale well.
Master goroutines, and you'll be able to write Go programs that handle thousands of concurrent operations with ease. The key is understanding when to use goroutines, how to synchronize them properly, and how to avoid common pitfalls like leaks and data races.
Practice with goroutines, experiment with different concurrency patterns, and leverage Go's powerful synchronization primitives. Goroutines are one of Go's standout features that make building concurrent systems enjoyable rather than painful.