Goroutines: Lightweight Concurrency
Goroutines are Go’s lightweight concurrency mechanism. They enable efficient concurrent programming with minimal overhead.
Creating Goroutines
Basic Goroutine
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Println(i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Start goroutine
go printNumbers()
// Main continues
fmt.Println("Main finished")
time.Sleep(1 * time.Second)
}
Multiple Goroutines
package main
import (
"fmt"
"time"
)
func worker(id int) {
for i := 0; i < 3; i++ {
fmt.Printf("Worker %d: %d\n", id, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Start multiple goroutines
for i := 1; i <= 3; i++ {
go worker(i)
}
time.Sleep(1 * time.Second)
}
Goroutine Synchronization
Using WaitGroup
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d started\n", id)
fmt.Printf("Worker %d finished\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers finished")
}
Using Channels for Synchronization
package main
import (
"fmt"
)
func worker(id int, done chan bool) {
fmt.Printf("Worker %d started\n", id)
fmt.Printf("Worker %d finished\n", id)
done <- true
}
func main() {
done := make(chan bool, 3)
for i := 1; i <= 3; i++ {
go worker(i, done)
}
// Wait for all workers
for i := 0; i < 3; i++ {
<-done
}
fmt.Println("All workers finished")
}
Goroutine Patterns
Fan-Out Pattern
package main
import (
"fmt"
"sync"
)
func process(id int, results chan int, wg *sync.WaitGroup) {
defer wg.Done()
results <- id * 2
}
func main() {
results := make(chan int, 5)
var wg sync.WaitGroup
// Fan-out: start multiple goroutines
for i := 1; i <= 5; i++ {
wg.Add(1)
go process(i, results, &wg)
}
// Fan-in: collect results
go func() {
wg.Wait()
close(results)
}()
for result := range results {
fmt.Println(result)
}
}
Worker Pool Pattern
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)
var wg sync.WaitGroup
// Start workers
numWorkers := 3
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, jobs, results, &wg)
}
// Send jobs
for j := 1; j <= 10; j++ {
jobs <- j
}
close(jobs)
// Wait for workers
wg.Wait()
close(results)
// Collect results
for result := range results {
fmt.Println(result)
}
}
Goroutine Lifecycle
Goroutine Cleanup
package main
import (
"fmt"
"time"
)
func worker(done <-chan bool) {
for {
select {
case <-done:
fmt.Println("Worker stopped")
return
default:
fmt.Println("Working...")
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
done := make(chan bool)
go worker(done)
time.Sleep(500 * time.Millisecond)
done <- true
time.Sleep(100 * time.Millisecond)
}
Best Practices
โ Good: Always Synchronize
// DO: Always wait for goroutines to finish
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// work
}()
wg.Wait()
โ Bad: Fire and Forget
// DON'T: Start goroutines without synchronization
go someFunction()
// Program might exit before goroutine completes
โ Good: Use Channels for Communication
// DO: Use channels for goroutine communication
results := make(chan int)
go func() {
results <- 42
}()
value := <-results
โ Good: Limit Goroutine Count
// DO: Use worker pools to limit goroutines
numWorkers := 10
for i := 0; i < numWorkers; i++ {
go worker(jobs, results)
}
Summary
Goroutines provide:
- Lightweight concurrency with minimal overhead
- Easy synchronization with channels and WaitGroup
- Efficient resource usage compared to threads
- Simple concurrent patterns for common tasks
- Scalable concurrency for high-performance applications
These features make Go ideal for concurrent programming.
Comments