Goroutines & ChannelsLESSON

Goroutines & Channels

Goroutines

A goroutine is a lightweight thread managed by the Go runtime. You start one by prefixing a function call with go:

Goroutines are extremely cheap — you can spawn thousands without issue. The key point: if main returns, all goroutines are killed immediately.

Channels

Channels are typed conduits that let goroutines communicate safely. Create one with make:

Send a value with ch <- value, and receive with value := <-ch. Channels synchronize the sender and receiver — both block until the other side is ready.

This example is complete and runnable: the main goroutine blocks on <-ch until sum sends its result.

Buffered Channels

A buffered channel accepts sends without a matching receiver — up to its capacity:

Unbuffered channels block on every send until a receiver is ready. Buffered channels only block when the buffer is full (send) or empty (receive).

The select Statement

select is like a switch for channel operations — it waits for whichever case is ready first:

If multiple cases are ready simultaneously, Go picks one at random. The default case runs immediately if no channel is ready, avoiding a block.

The Default Case in select

A default case makes channel operations non-blocking — instead of waiting for a channel, Go immediately takes the default branch when no case is ready.

Try-receive pattern — check if a value is available without blocking:

Try-send pattern — drop a value rather than block when the channel is full:

Polling loopdefault combined with time.Sleep lets a goroutine periodically check a channel without ever blocking on it:

Typical use cases: rate limiting, circuit breakers, and checking whether a goroutine has finished without stalling the caller.

Range and Close

Closing a channel

close(ch) signals to receivers that no more values will be sent. Only the sender should close a channel — closing a nil channel or closing one that is already closed will panic.

The comma-ok idiom

A receive expression returns a second boolean that tells you whether the channel is still open:

for range over a channel

for range handles the comma-ok check internally and stops the loop automatically when the channel is closed and empty:

This is the standard producer/consumer pattern: the producer goroutine closes the channel when it is done; the consumer uses for range rather than counting expected values.

Rules to avoid panics:

  • Never close a channel from the receiver side
  • Never close a channel more than once
  • If multiple goroutines may send, coordinate closure with a sync.WaitGroup and a dedicated closer goroutine

sync.WaitGroup

time.Sleep is a poor way to wait for goroutines. The idiomatic solution is sync.WaitGroup:

  • wg.Add(n) — increment the counter by n before launching goroutines
  • wg.Done() — decrement the counter (always use with defer)
  • wg.Wait() — block until the counter reaches zero

Channel Directions

Function signatures can constrain a channel to send-only or receive-only, preventing accidental misuse:

SyntaxMeaning
chan TBidirectional (read and write)
chan<- TSend-only
<-chan TReceive-only

Directional channels improve safety and serve as documentation — the signature tells you exactly how the channel is used.

sync.Mutex

When multiple goroutines share mutable state, protect it with a sync.Mutex:

The defer c.mu.Unlock() pattern ensures the mutex is always released, even if the function returns early or panics.

Goroutine Leaks

A goroutine that is never able to complete is a goroutine leak — it consumes memory and CPU indefinitely. The most common cause is a goroutine blocked on a channel receive when no sender will ever send:

To prevent leaks: always ensure goroutines have a way to exit — use a done channel, a context.Context cancellation, or close the channel when work is complete.

Knowledge Check

In a channel type `chan<- int`, what does `chan<-` mean?

What does a `select` statement do?

What happens when a select statement has a default case and no channel is ready?