Goroutines are Go’s lightweight concurrency mechanism. They enable efficient concurrent programming with minimal overhead. For more context, see Go Installation Guide, Go Ecosystem Overview, Go Best Practices.
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