Free tools Windows power users keep installed
One-click scans. No signup required.
Goroutines start work, channels move values and establish ordering between goroutines, and select waits on several channel operations at once. None of the three makes shared data safe on its own, and select does not favor the first case you write. Once you know exactly what each primitive guarantees, the rest of Go’s concurrency model follows from those guarantees.
How do goroutines work in Go?
A goroutine is a function executing concurrently with other goroutines in the same address space. The go statement starts a function call and returns immediately without waiting for the call to finish. When that function returns, its goroutine exits. Go’s Effective Go guide describes goroutines as independently executing functions that the runtime multiplexes onto operating-system threads, so a blocked goroutine does not have to hold up the rest of the program.
As an Amazon Associate I earn from qualifying purchases.
Two distinctions prevent most early mistakes:
- Goroutines are not OS threads. The runtime schedules them onto threads, and you should not reason about thread identity or thread count from the goroutine count in your code.
- Concurrency is not parallelism. Concurrency is a way of structuring independently progressing work. Parallelism is simultaneous execution on multiple processors. The Go FAQ makes this point directly: a program can be concurrent on one processor, and a concurrent design only runs in parallel when the hardware and the workload allow it. Adding goroutines to sequential work does not make it faster, and coordination between goroutines has a cost that can outweigh the gain.
Goroutines are cheap relative to threads in most programs, but the official documentation does not give you a stable number to plan against, so design for lifetime and count rather than for an assumed per-goroutine cost.
What does a channel guarantee?
A channel is a typed conduit. Its element type is fixed when you create it, and each operation on it is either a send (ch <- v) or a receive (<-ch). The Go Programming Language Specification defines when those operations can proceed, and that definition is what you should rely on when reasoning about a program.
Unbuffered channels: rendezvous
An unbuffered channel has capacity zero. A send and a receive complete together: the sender cannot finish until a receiver is ready to take the value, and the receiver cannot finish until a sender provides one. This is why an unbuffered channel is the simplest tool for “wait until that other goroutine has reached this point.” Because the two sides meet, the operation also orders the work around it.
Buffered channels: decoupling with limits
A buffered channel with capacity n lets a send complete while there is room in the buffer, and lets a receive complete while the buffer holds a value. A send to a buffered channel can therefore finish before any receiver has taken the value. The buffer gives you temporary queueing and absorbs bursts, but it does not remove the need to decide who owns the value, who closes the channel, and what happens when work stops early. A full buffer blocks senders in exactly the same way an unbuffered channel does, which is the backpressure you are buying with the capacity.
Closing and nil channels
Closing a channel signals that no more values will be sent; receivers still get any values already buffered, then receive the zero value with a false flag. Only the sender side should close, and closing a channel twice or sending to a closed channel panics. A nil channel blocks forever on both send and receive, which is useful inside select to disable a case. Those rules come from the specification and are worth reading in full before you build shutdown logic.
How does select work in Go?
select waits on several channel communications at once. Its behavior comes from the specification’s rules, and each rule matters in practice.
- Evaluation happens once, on entry. Every case’s channel operand and every send value are evaluated exactly once, in source order, before any communication is chosen. Side effects in those expressions happen even for a case that is not selected.
- If any communications are ready, one is chosen uniformly at random. The specification calls for uniform pseudo-random choice among ready cases. Textual order gives no priority, so a case written first is not preferred over one written later.
- If none is ready and a
defaultcase exists, it runs immediately. Thedefaultcase turns the statement into a non-blocking check. - If none is ready and there is no
default, the statement blocks until one case can proceed.
The random choice has a practical consequence. If two channels both have values waiting, a loop that always reads from the first case in source order will not starve the second, but you also cannot assume which one arrives first. Code that needs a particular priority must express it explicitly, for example by draining a high-priority channel in a separate step before entering the general select.
Blocking select: waiting for the first event
select {
case msg := <-messages:
fmt.Println("message:", msg)
case err := <-errs:
fmt.Println("failed:", err)
}
This statement waits until either channel has a value. If both are ready at that moment, either case may run.
Rank #3
Non-blocking select: checking without waiting
select {
case msg := <-messages:
fmt.Println("message:", msg)
default:
// nothing ready; continue with other work
}
The default case means “do not wait.” It is useful when a goroutine has other work to do and only wants to pick up a message if one is already waiting. It is not a yield or a sleep. If you put it inside a for loop with nothing else to do, the loop spins and consumes CPU, so use it for genuine fallback behavior, not as a polling substitute for a blocking receive.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Timeouts and cancellation do not stop the work
A select with a timeout case lets the caller stop waiting. It does not stop the goroutine doing the work. The Go blog’s “Timing out, moving on” article, first published around 2010, demonstrates this pattern with a buffered result channel. The buffer matters: if the caller has timed out and moved on, a late worker can still send its result without blocking forever, and then exit.
results := make(chan string, 1) // capacity 1: a late send does not block
go func() {
time.Sleep(2 * time.Second) // stands in for slow work
results <- "done"
}()
select {
case r := <-results:
fmt.Println("got:", r)
case <-time.After(500 * time.Millisecond):
fmt.Println("gave up waiting; the worker keeps running")
}
In this program the worker is not cancelled when the timeout fires. It finishes its sleep, sends into the buffer, and exits. If the worker did real I/O or computation, the caller would have no way to make it stop early. For that you need a cancellation signal the worker actually checks. Modern Go code usually passes a context.Context and has the worker watch ctx.Done() in its own select:
Rank #4
func work(ctx context.Context, out chan<- string) {
select {
case <-time.After(2 * time.Second):
out <- "done"
case <-ctx.Done():
return // caller gave up; exit without sending
}
}
Note that time.After creates a timer that is not released until it fires. In a long-running loop, prefer a single time.Timer or a context deadline.
Bounding how many goroutines run
Starting one goroutine per task is simple, but the number of waiting goroutines then grows with the input. A common mistake is to launch a goroutine for every item and limit only the expensive work inside each goroutine. The goroutines still exist and still hold their stacks while they wait. Effective Go’s concurrency examples gate goroutine creation itself or use a fixed set of workers that read from a shared channel.
jobs := make(chan Job)
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := range jobs {
process(j)
}
}()
}
for _, j := range inputs {
jobs <- j
}
close(jobs)
wg.Wait()
The worker count sets the concurrency ceiling, and the unbuffered jobs channel applies backpressure to the producer. Since Go 1.22, each iteration of a three-clause for loop gets its own copy of the loop variable, so closures capturing i behave as most readers expect. Code that must run on older toolchains needs an explicit copy inside the loop. Effective Go’s own examples predate that change and flag the issue.
Best Value
Channels do not make shared memory safe
The Effective Go slogan is “Do not communicate by sharing memory; instead, share memory by communicating.” It is a useful design preference, not a safety guarantee. The Go Memory Model is explicit about the underlying rule: programs that modify data being simultaneously accessed by multiple goroutines must serialize that access. A channel send and receive establish a happens-before relationship for the values they carry, but a goroutine that writes a shared map without coordination still races with any other goroutine touching it.
Effective Go also says the approach can be taken too far. For simple shared state, such as a counter guarded by a lock, a sync.Mutex or an atomic operation from sync/atomic is often clearer than a dedicated goroutine and channel. Large concurrent programs commonly use both. The Go FAQ notes that concurrency toolkits are complementary, and the right choice depends on the problem.
To check for races while you develop, build and test with the race detector (go test -race or go run -race). It finds races that actually execute during a run, so it complements reasoning from the memory model rather than replacing it.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallChoosing between the options
| Decision | Option A | Option B | Choose when |
|---|---|---|---|
| Channel type | Unbuffered: rendezvous, immediate backpressure | Buffered: temporary queueing, limited decoupling | Use unbuffered when you need the handoff to synchronize; use buffered when a bounded burst should not block the producer |
| Protecting shared state | Channel ownership: one goroutine owns the data and others send requests | sync.Mutex or sync/atomic |
Use ownership when the data flows through a pipeline; use a lock or atomic for a small shared value |
| Goroutine launch | One goroutine per task | Fixed worker set or gated admission | Use one per task only when the task count is small and bounded; use a worker set when input size is unbounded |
| Waiting style | Blocking select |
select with default |
Block to wait for an event; use default only for a genuine fallback path that does not spin |
Common mistakes to correct
- Do not write that
selectpicks the first ready case. Ready cases are chosen uniformly at random. - Do not write that
defaultwaits, yields, or sleeps. It runs when no communication can proceed. - Do not assume a channel removes the need for synchronization on shared mutable data.
- Do not assume a goroutine ends when its parent function returns or when a timeout fires. Lifetime has to be designed with a completion signal or a cancellation check.
- Do not equate concurrency with speed. Gains depend on whether the work divides and whether the useful computation outweighs coordination overhead.
Where to read next
For normative behavior, read the Go Programming Language Specification (the version current at the time of your reading; the documentation index on go.dev lists the release you are looking at) and the Go Memory Model (dated June 6, 2022 in its current form). For guided learning, the Effective Go concurrency section and the Go Wiki’s LearnConcurrency page are the official starting points. The official Go learning page also lists books and training providers; The Go Programming Language by Alan A. A. Donovan and Brian W. Kernighan is a long-standing reference, but confirm the current edition before you buy.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

