logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·30.10.26
← Back to news

Go Concurrency Distilled

Original article
antonz.org|332 points|140 comments|by chmaynard|Sep 26, 2026

Go Concurrency Distilled

Authored by Anton Zhiyanov

This condensed guide offers a high-level summary of various concurrency patterns and primitives within the Go programming language.

Note: This resource is intended as a beginner's tutorial quick refresher for those already familiar with the basics. If you are starting from scratch, please refer to Gist of Go: Concurrency.

🗺️ Roadmap of Topics

  • Goroutines
  • Channels
  • Select
  • Pipelines
  • Time
  • Context
  • Wait groups
  • Data races
  • Race conditions
  • Mutexes
  • Semaphores
  • Signaling
  • Run once
  • Object pool
  • Atomics
  • Testing
  • Scheduling
  • Diagnostics
  • Final thoughts

🚀 Goroutines

The core of Go's concurrency model is the goroutine. A goroutine is a lightweight thread managed by the Go runtime, initiated simply by prefixing a function call with the go keyword.

How it Works

The Go runtime acts as an intermediary, multiplexing goroutines onto a smaller number of actual operating system (OS) threads.

Key Characteristics:

  • Efficiency: Goroutines are significantly cheaper than OS threads. While an OS thread might require megabytes of stack space, a goroutine starts with a few kilobytes.
  • Implicit Entry: The main function is itself a goroutine that triggers automatically upon program execution.
  • Resource Cost: Memorygoroutine≪MemoryOS thread\text{Memory}_{\text{goroutine}} \ll \text{Memory}_{\text{OS thread}}Memorygoroutine​≪MemoryOS thread​.

Synchronization with sync.WaitGroup

Because the main goroutine does not automatically wait for others to finish, we use a sync.WaitGroup to prevent the program from exiting prematurely.

  • Add(n): Increases the internal counter by nnn.
  • Done(): Decreases the counter by 111.
  • Wait(): Blocks the current goroutine until the counter hits 000.
func main() {
    var wg sync.WaitGroup
    wg.Add(2) // Expecting two workers

    go worker(1, &wg)
    go worker(2, &wg)

    wg.Wait() // Block until both call Done()
}

Pro Tip: WaitGroup.Go is a helper that encapsulates the Add, execution, and Done steps into a single call.


📨 Channels

Channels serve as the communication pipes that allow goroutines to synchronize and exchange data. Think of a channel as a conveyor belt or a window where one party drops off a value and another picks it up.

Basic Communication

Sending (ch <- val) and receiving (val := <-ch) are synchronous (blocking) operations by default. The sender blocks until a receiver is ready, and vice versa.

func main() {
    messages := make(chan string)
    go func() {
        messages <- "ping" // Blocks until main receives
    }()
    msg := <-messages // Blocks until goroutine sends
    fmt.Println(msg) // Output: ping
}

The Output Channel Pattern

A frequent Go idiom involves a function creating a channel, launching a background goroutine to populate it, and returning the channel to the caller.

func generate(start, stop int) chan int {
    out := make(chan int)
    go func() {
        for i := start; i < stop; i++ {
            out <- i
        }
    }()
    return out
}

Closing Channels

The close() function signals to receivers that no more data will be sent.

The "Comma OK" Idiom: When reading from a channel, you can determine if it is still open using a second boolean return value: val, ok := <-ch

  • ok == true: Value was received from an open channel.
  • ok == false: Channel is closed and empty; val is the zero-value of the type.

Warning: Writing to a closed channel or closing a channel twice will trigger a panic.

You must always close every channel. →\rightarrow→ Correction: Only close a channel if the receiver needs to know that the stream has ended. Otherwise, the Garbage Collector (GC) will handle it.

Iteration with range

The range keyword simplifies reading until a channel is closed:

for n := range nums {
    fmt.Print(n, " ")
}

Note: Unlike slices, range over a channel returns only the value, not the index.

Directional Channels

To increase type safety and prevent logic errors, you can specify if a channel is only for sending or receiving.

SyntaxDirectionCapability
chan TBidirectionalRead and Write
chan<- TSend-onlyWrite only
<-chan TReceive-onlyRead only

Go automatically casts bidirectional channels to directional ones when passed into functions.

Buffered Channels

Buffered channels act as FIFO queues with a capacity CCC.

  • Non-blocking Write: Occurs as long as len(ch) < cap(ch).
  • Non-blocking Read: Occurs as long as len(ch) > 0.
stream := make(chan int, 3) // Buffer size of 3
stream <- 11
stream <- 12
stream <- 13
fmt.Println(<-stream) // 11

The nil Channel

A channel's zero value is nil.

  • Reading/Writing to a nil channel →\rightarrow→ Blocks forever.
  • Closing a nil channel →\rightarrow→ Panic.

⚡ Select

The select statement is somewhat l... (Text ends here)