Concurrency

Go’s concurrency model is built on goroutines and channels. The mantra: “Don’t communicate by sharing memory; share memory by communicating.”

Goroutines

A goroutine is a lightweight thread managed by the Go runtime. Each starts with ~2KB of stack (grows as needed). You can run millions of them.

func main() {
    go sayHello("world") // launches a goroutine
    time.Sleep(time.Second)
}

func sayHello(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

Any function call prefixed with go runs concurrently. The main goroutine does not wait for others to finish unless you explicitly synchronize.

func main() {
    var wg sync.WaitGroup

    for i := range 5 {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println("worker", i)
        }()
    }

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

Tip: Since Go 1.22, loop variables are per-iteration. No need to capture i in a closure parameter anymore.

Channels

Channels are typed conduits for sending and receiving values between goroutines.

// Create a channel
ch := make(chan string)

// Send (blocks until someone receives)
go func() {
    ch <- "hello"
}()

// Receive (blocks until someone sends)
msg := <-ch
fmt.Println(msg) // "hello"

Buffered vs Unbuffered

TypeCreationSend blocks whenReceive blocks when
Unbufferedmake(chan T)No receiver readyNo sender ready
Bufferedmake(chan T, n)Buffer fullBuffer empty
// Buffered channel - send doesn't block until buffer is full
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 would block here (buffer full)

Directional Channels

Restrict channel usage in function signatures:

func producer(out chan<- int) { // send-only
    for i := range 10 {
        out <- i
    }
    close(out)
}

func consumer(in <-chan int) { // receive-only
    for v := range in {
        fmt.Println(v)
    }
}

Select

select lets a goroutine wait on multiple channel operations. It blocks until one case is ready, picking randomly if multiple are ready simultaneously.

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "one"
    }()
    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "two"
    }()

    select {
    case msg := <-ch1:
        fmt.Println("received", msg)
    case msg := <-ch2:
        fmt.Println("received", msg)
    }
}

Use default for non-blocking operations:

select {
case msg := <-ch:
    fmt.Println(msg)
default:
    fmt.Println("no message ready")
}

Context

context.Context carries deadlines, cancellation signals, and request-scoped values across goroutines and API boundaries.

func fetchData(ctx context.Context, url string) ([]byte, error) {
    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
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

func main() {
    // Cancel after 5 seconds
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    data, err := fetchData(ctx, "https://api.example.com/data")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(data))
}

Context Rules

  1. Pass ctx as the first parameter: func DoThing(ctx context.Context, ...)
  2. Never store contexts in structs
  3. Always call cancel() (use defer)
  4. Use context.Background() at the top of your call chain
  5. Use context.TODO() when you’re unsure which context to use (temporary)

Gotcha: A cancelled context cancels all derived contexts. If you create childCtx from parentCtx and cancel the parent, the child is also cancelled.

Quick Reference

go f()                              // launch goroutine
ch := make(chan T)                   // unbuffered channel
ch := make(chan T, n)                // buffered channel
ch <- v                             // send
v := <-ch                           // receive
close(ch)                           // close (only sender closes)
for v := range ch { }               // receive until closed
select { case <-ch: ... }           // multiplex channels
ctx, cancel := context.WithTimeout(ctx, d) // deadline

Next: Interfaces | Concurrency Patterns