Web Analytics

Goroutines

Advanced ~40 min read

Goroutines are lightweight threads managed by the Go runtime. They're one of Go's most powerful features, making concurrent programming simple and efficient. In this lesson, you'll learn how goroutines work and how to use them effectively.

What is a Goroutine?

A goroutine is a lightweight thread of execution. Unlike OS threads, goroutines are managed by the Go runtime and are extremely cheap to create.

Goroutines vs OS Threads OS Threads Heavy (~1-2 MB) OS Managed Expensive to create Context switching Goroutines Lightweight (~2 KB) Go Runtime Managed Cheap to create Efficient scheduling Thousands of goroutines = Few OS threads 1 Thread 1000s Goroutines
Goroutines are much lighter than OS threads
Goroutine Characteristics:
  • Lightweight - Start with ~2KB stack (grows as needed)
  • Cheap - Can create millions of goroutines
  • Multiplexed - Many goroutines run on few OS threads
  • Managed - Go runtime handles scheduling

Creating Goroutines

Use the go keyword to start a goroutine:

Output
Click Run to execute your code
Goroutine Execution Flow Main Goroutine 1 Start 2 go func() 3 Continue 4 End spawns Goroutine 1 Runs concurrently with main Goroutine 2 Independent execution
Main goroutine spawns child goroutines that run concurrently

Common Goroutine Patterns

1. Anonymous Function Goroutines

Output
Click Run to execute your code

2. Multiple Goroutines

func main() {
    for i := 0; i < 5; i++ {
        go func(id int) {
            fmt.Printf("Goroutine %d starting\n", id)
            time.Sleep(time.Second)
            fmt.Printf("Goroutine %d done\n", id)
        }(i)  // Pass i as argument!
    }
    
    time.Sleep(2 * time.Second)  // Wait for goroutines
}
Important: Always pass loop variables as arguments to goroutines! Otherwise, all goroutines will see the final value of the loop variable.

Synchronizing with WaitGroups

Use sync.WaitGroup to wait for goroutines to finish:

Output
Click Run to execute your code
WaitGroup Synchronization Main wg.Add(3) wg.Wait() Continue Go 1 wg.Done() Go 2 wg.Done() Go 3 wg.Done() Blocked until all Done()
WaitGroup blocks main until all goroutines call Done()
WaitGroup Methods:
  • Add(n) - Increment counter by n
  • Done() - Decrement counter by 1
  • Wait() - Block until counter is 0

How Goroutines Are Scheduled

Go Scheduler: M:N Model Goroutines (G) G1 G2 G3 G4 G5 G6 Go Runtime Scheduler Multiplexes M goroutines onto N OS threads OS Threads (M) Thread 1 Thread 2 Thread 3
Many goroutines (M) run on few OS threads (N)
Pro Tip: The Go scheduler uses a work-stealing algorithm to balance load across OS threads. You don't need to manage thisβ€”it happens automatically!

Common Mistakes

1. Not waiting for goroutines

// ❌ Wrong - main exits before goroutine runs
func main() {
    go fmt.Println("Hello")
    // Program exits immediately!
}

// βœ… Correct - wait for goroutine
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println("Hello")
    }()
    wg.Wait()
}

2. Capturing loop variables

// ❌ Wrong - all goroutines see final value
for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i)  // All print 5!
    }()
}

// βœ… Correct - pass as argument
for i := 0; i < 5; i++ {
    go func(n int) {
        fmt.Println(n)  // Prints 0, 1, 2, 3, 4
    }(i)
}

3. Goroutine leaks

// ❌ Wrong - goroutine never exits
func leak() {
    go func() {
        for {
            // Infinite loop with no exit condition
            time.Sleep(time.Second)
        }
    }()
}

// βœ… Correct - provide exit mechanism
func noLeak(ctx context.Context) {
    go func() {
        for {
            select {
            case <-ctx.Done():
                return  // Exit when context cancelled
            default:
                time.Sleep(time.Second)
            }
        }
    }()
}

Exercise: Concurrent URL Fetcher

Task: Create a concurrent URL fetcher using goroutines.

Requirements:

  • Fetch multiple URLs concurrently
  • Use WaitGroup to wait for all fetches
  • Print the status of each fetch
  • Measure total time taken
Show Solution
package main

import (
    "fmt"
    "net/http"
    "sync"
    "time"
)

func fetchURL(url string, wg *sync.WaitGroup) {
    defer wg.Done()
    
    start := time.Now()
    resp, err := http.Get(url)
    duration := time.Since(start)
    
    if err != nil {
        fmt.Printf("❌ %s - Error: %v (%.2fs)\n", url, err, duration.Seconds())
        return
    }
    defer resp.Body.Close()
    
    fmt.Printf("βœ… %s - Status: %s (%.2fs)\n", url, resp.Status, duration.Seconds())
}

func main() {
    urls := []string{
        "https://golang.org",
        "https://google.com",
        "https://github.com",
        "https://stackoverflow.com",
        "https://reddit.com",
    }
    
    var wg sync.WaitGroup
    start := time.Now()
    
    fmt.Println("Fetching URLs concurrently...")
    fmt.Println("==============================")
    
    for _, url := range urls {
        wg.Add(1)
        go fetchURL(url, &wg)
    }
    
    wg.Wait()
    
    totalDuration := time.Since(start)
    fmt.Println("==============================")
    fmt.Printf("Total time: %.2fs\n", totalDuration.Seconds())
    fmt.Printf("Fetched %d URLs concurrently\n", len(urls))
}

Summary

  • Goroutines are lightweight threads managed by Go runtime
  • go keyword starts a new goroutine
  • Very cheap - can create millions of goroutines
  • WaitGroup synchronizes goroutine completion
  • Pass loop variables as arguments to goroutines
  • M:N scheduling - many goroutines on few OS threads
  • Always provide exit mechanisms to avoid leaks

What's Next?

Now that you understand goroutines, you're ready to learn about Channelsβ€”Go's way of communicating between goroutines. Channels make concurrent programming safe and elegant!