Go Goroutines & Concurrency

Launching Goroutines

go keyword — lightweight concurrent execution
func main() {
    go doWork("task-1")
    go doWork("task-2")
    time.Sleep(time.Second) // crude wait — use WaitGroup in real code
}

func doWork(name string) {
    fmt.Printf("%s: started\n", name)
    time.Sleep(500 * time.Millisecond)
    fmt.Printf("%s: done\n", name)
}

go f() launches f on a new goroutine — a lightweight, runtime-managed thread. Goroutines cost about 2KB of stack (grows as needed). You can run thousands without issue.

WaitGroup

sync.WaitGroup coordinates goroutine lifecycle
var wg sync.WaitGroup
urls := []string{
    "https://example.com",
    "https://httpbin.org/get",
}

for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        resp, err := http.Get(u)
        if err != nil {
            fmt.Printf("FAIL %s: %v\n", u, err)
            return
        }
        resp.Body.Close()
        fmt.Printf("OK   %s: %d\n", u, resp.StatusCode)
    }(url)
}

wg.Wait() // blocks until all goroutines call Done()

Add(1) before launching, defer Done() inside the goroutine, Wait() in the caller.

Mutex

sync.Mutex prevents data races
type SafeCounter struct {
    mu sync.Mutex
    v  map[string]int
}

func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.v[key]++
}

func (c *SafeCounter) Get(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.v[key]
}

defer c.mu.Unlock() ensures the lock is released even on panic. sync.RWMutex allows concurrent reads with RLock()/RUnlock().

Worker Pool

Bounded concurrency via goroutines and channels
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 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send 9 jobs
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // Collect results
    for r := 1; r <= 9; r++ {
        fmt.Println(<-results)
    }
}

The worker pool bounds concurrency to N goroutines. Workers read from a shared jobs channel. close(jobs) signals no more work.

Context

context.Context for cancellation and timeouts
func fetchWithTimeout(url string) ([]byte, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err // includes context.DeadlineExceeded
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

context.WithTimeout creates a context that cancels after a deadline. defer cancel() releases resources immediately when the function returns. Pass context as the first parameter to every function that does I/O.