Learning Go: Goroutines, WaitGroups & Channels

Published on Monday, Apr 20, 2026

1 min read


Go approaches concurrency in a way that feels deceptively simple, yet extremely powerful. Instead of wrestling with threads, locks, and race conditions, Go gives you a cleaner abstraction: goroutines and channels.

This guide walks through three core primitives:

  • Goroutines
  • WaitGroups
  • Channels

Goroutines — Lightweight Concurrency

A goroutine is a lightweight thread managed by the Go runtime.

Unlike OS threads:

  • They start with ~2KB stack
  • They grow dynamically
  • You can create thousands or even millions of them

Threads vs Goroutines

FeatureThreadsGoroutines
Managed byOSGo runtime
Memory~1MB~2KB
CostExpensiveCheap
SchedulingOSGo scheduler

Go uses an M:N scheduler: Many goroutines are multiplexed onto fewer OS threads.

Example

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Hello from goroutine")
}

func main() {
    go sayHello()
    fmt.Println("Hello from main")
    time.Sleep(1 * time.Second)
}

Key point: When main() exits, all goroutines terminate immediately.


WaitGroup — Coordination Primitive

When multiple goroutines are running, you often need to wait for all of them to complete.

sync.WaitGroup helps coordinate this.

Analogy

Think of a watchman:

  • You tell him how many tasks are running
  • Each task reports when done
  • He lets you proceed only when all are complete

Example

package main

import (
    "fmt"
    "sync"
)

func readFile(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Reading file", id)
}

func main() {
    var wg sync.WaitGroup
    totalFiles := 5

    wg.Add(totalFiles)

    for i := 1; i <= totalFiles; i++ {
        go readFile(i, &wg)
    }

    wg.Wait()
    fmt.Println("All files processed")
}

Common Mistakes

  • Forgetting wg.Done()
  • Calling wg.Add() inside goroutine

Channels — Communication Mechanism

Channels allow safe communication between goroutines.

Instead of sharing memory directly, Go encourages: “Share memory by communicating”

Syntax

ch <- value   // send
value := <-ch // receive

Example

package main

import "fmt"

func main() {
    ch := make(chan int)

    go func() {
        for i := 1; i <= 10; i++ {
            ch <- i
        }
        close(ch)
    }()

    for val := range ch {
        fmt.Println(val)
    }
}

Behavior

  • Channels are blocking by default
  • Send blocks until receive
  • Receive blocks until send

Buffered Channels

ch := make(chan int, 3)

Allows limited non-blocking sends.


Summary

  • Goroutines are lightweight concurrent workers
  • WaitGroups coordinate execution
  • Channels enable safe communication

Go’s concurrency model emphasizes simplicity and correctness over complexity.


Next Steps

  • Explore select statements
  • Learn context for cancellation
  • Build producer-consumer systems