Channels in Go provide a way for goroutines to communicate by sending and receiving values.
Declare:
ch := make(chan int)
The channel type defines what kind of data it can carry. It can be any type, including named types.
Send:
ch <- 10
Receive:
value := <-ch
Close:
close(ch)
Closing a closed channel causes a panic.
Channel synchronization coordinates communication between goroutines. It ensures data is not lost and preserves the correct order.
Send operation: Blocks the current goroutine until another goroutine (the other side) is ready to receive.
Receive operation: Blocks the current goroutine until a value is available (from the other side).
Whoever comes first has to wait for the other one.
Example:
func main() { ch := make(chan string) go expensiveFunc("Hello", ch) fmt.Println("Main") for range 4 { fmt.Println(<-ch) } fmt.Println("End")}func expensiveFunc(text string, ch chan string) { for i := range 4 { time.Sleep(500 * time.Millisecond) ch <- text + " " + fmt.Sprint(i) }}// Main// Hello 0// Hello 1// Hello 2// Hello 3// End
No extra mechanism is required in main to wait for the goroutine. The <-ch operation blocks until a value is available. This behavior synchronizes main with expensiveFunc. Each loop iteration in main waits for a send from expensiveFunc. The loop is used only as an example, calling fmt.Println(<-ch) 4 times back to back would do the same.
In the example above, closing the channel is not required because main receives a fixed number of messages (4) before exiting. Closing is only necessary when the receiver must be told there are no more values coming, such as to terminate a range loop.
Here is a modified version that requires the channel to be closed explicitly:
func main() { ch := make(chan string) go expensiveFunc("Hello", ch) fmt.Println("Main") for msg := range ch { fmt.Println(msg) } fmt.Println("Done.")}func expensiveFunc(text string, ch chan string) { defer close(ch) for i := range 4 { time.Sleep(500 * time.Millisecond) ch <- text + " " + fmt.Sprint(i) }}// Main// Hello 0// Hello 1// Hello 2// Hello 3// Done.
The for msg := range ch { ... } syntax performs msg := <-ch internally, where blocking occurs.
range does not know how many values a channel will receive, it may be infinite. To stop the loop, close the channel to signal that no more values will be sent.
Receiving from a closed unbuffered channel returns the zero-value immediately.
The select statement is a control structure for working with multiple channels simultaneously. It is similar to switch statement, but designed for channel operations.
How it works:
select listens on multiple channels.
It executes the first ready case.
If multiple cases are ready, one is chosen randomly.
Without a default, it blocks until a case is ready.
With a default, it executes the default itself immediately if no cases are ready.
A select statement is not a loop, it executes one case even if multiple are ready and then exits.
In the example above, two goroutines remain blocked after select receives from one channel because the other two
channels are never received. In a short-lived program like this, it is not noticeable because the program exits
immediately afterward. However, in a long-running program, those blocked goroutines never terminate, causing a
goroutine leak that also keeps their memory allocated.
This can be avoided using any cancellation mechanism.
The Done Channel pattern signals cancellation or completion in goroutines. It uses a separate shared channel, which is closed to notify them to stop execution.
Without a Done Channel:
func myFunc() { for { fmt.Println("Working...") }}func main() { go myFunc() time.Sleep(1 * time.Hour)}
The myFunc goroutine prints "Working..." continuously until the program exits.
The myFunc goroutine prints "Working..." for 5 seconds and then returns, even though the main program continues to run for an hour.
The struct{} type represents an empty struct in Go. It consumes zero bytes of memory, making it ideal for
signaling and control purposes without causing any memory overhead.
The Fan-In pattern combines multiple input channels into a single output channel so one consumer can read from all sources.
ch1 ----\ \ ---> merge ---> out /ch2 ----/
Example:
func merge(chs ...<-chan string) <-chan string { out := make(chan string) var wg sync.WaitGroup wg.Add(len(chs)) for _, ch := range chs { go func(ch <-chan string) { defer wg.Done() for v := range ch { out <- v } }(ch) } go func() { wg.Wait() close(out) }() return out}func myFunc(nums ...int) <-chan string { ch := make(chan string) go func() { defer close(ch) // Example business logic that writes output to the channel: for _, n := range nums { ch <- fmt.Sprintf("Hi %v!", n) } }() return ch}func main() { a := myFunc(1, 3, 5) b := myFunc(2, 4, 6) for v := range merge(a, b) { fmt.Println(v) }}// Hi 1!// Hi 2!// Hi 4!// Hi 3!// Hi 6!// Hi 5!
Output order is not guaranteed because values are forwarded as soon as they arrive from either channel.
In Go, a context is a mechanism for propagating cancellation signals, deadlines, timeouts, and request-scoped values through function calls and goroutines.
The hierarchy:
Contexts form an immutable tree structure (we do not modify a context, we add subtrees to a context).
Child contexts inherit cancellation, deadlines, and values from their parent.
Cancelling a parent cancels all of its descendants.
A child cannot cancel its parent or its siblings.
How it works:
WithCancel() (or one of the other variants) creates a child context and returns a cancellation function.
Every context provides a Done channel (ctx.Done()).
Calling cancel() (or when a timeout expires or a deadline is reached) closes the ctx.Done() channel.
Anyone depending on that context is immediately notified through the ctx.Done() channel.
ctx.Err() returns the reason the context ended (nil if it hasn't ended yet).
Context only signals cancellation by closing the ctx.Done() channel. It does not stop your code automatically. Every
function or goroutine that accepts a context is responsible for checking ctx.Done() and implementing its own
cancellation or cleanup logic.
Creating Contexts:
The root context, the starting point for creating child contexts.