A channel lets goroutines communicate by sending and receiving typed values. A chan string carries strings, a chan int carries integers, and a chan Job carries values of a custom Job type.
Channels are one of Golang’s core concurrency tools. They are useful when one goroutine produces data and another goroutine consumes it, or when several workers need to coordinate results.
package main
import "fmt"
func main() {
messages := make(chan string)
go func() {
messages <- "tutorial loaded"
}()
message := <-messages
fmt.Println(message)
}
An unbuffered channel synchronizes sender and receiver. A send blocks until another goroutine receives the value. A receive blocks until another goroutine sends a value.
This behavior is useful when you want handoff semantics: the sender knows another goroutine has accepted the value before the send continues.
done := make(chan bool)
go func() {
fmt.Println("saving file")
done <- true
}()
<-done
fmt.Println("save completed")
A buffered channel can hold a limited number of values before the sender blocks. Create one by passing a capacity to make, such as make(chan string, 3).
Buffers are useful for smoothing small bursts of work, but they are not a replacement for good flow-control design. If the buffer fills, sends still block. If consumers are too slow, a larger buffer only delays the problem.
| Channel Type | Behavior | Common Use |
|---|---|---|
| Unbuffered | Sender and receiver meet at the same time | Direct handoff and synchronization |
| Buffered | Channel stores values up to its capacity | Small queues and burst smoothing |
| Closed | Receivers can drain remaining values, then stop | Signal that no more values will arrive |
queue := make(chan string, 2)
queue <- "job-1"
queue <- "job-2"
fmt.Println(<-queue)
fmt.Println(<-queue)
A sender can close a channel to signal that no more values will arrive. Receivers can range over the channel until it is closed and drained.
Only the sender should close a channel. Closing from the receiver side can panic if another sender sends later. Sending on a closed channel also panics, so ownership of closing should be clear.
jobs := make(chan string)
go func() {
defer close(jobs)
jobs <- "resize-image"
jobs <- "send-email"
}()
for job := range jobs {
fmt.Println("processing", job)
}
Function parameters can restrict a channel to send-only or receive-only. This documents intent and prevents accidental misuse inside the function.
Use chan<- T for send-only parameters and <-chan T for receive-only parameters. Directional channel types are common in worker and pipeline code.
func produce(out chan<- string) {
out <- "first"
out <- "second"
close(out)
}
func consume(in <-chan string) {
for value := range in {
fmt.Println(value)
}
}
select waits on multiple channel operations and runs the case that becomes ready first. It is common for timeouts, cancellation, fan-in patterns, and coordinating worker results.
A default case makes select non-blocking. Use it carefully because a tight loop with a default case can spin and waste CPU.
select {
case value := <-result:
fmt.Println(value)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
Channels are great for communication and ownership transfer. Mutexes are great for protecting shared state. Do not force every concurrency problem into channels just because channels are distinctive in Golang.
A good rule of thumb: use a channel when goroutines need to pass work, results, or signals. Use a mutex when several goroutines need controlled access to the same in-memory state.
| Need | Often Use |
|---|---|
| Pass jobs to workers | Channel |
| Collect results from workers | Channel |
| Cancel a running operation | Context or closed channel |
| Protect a shared counter or map | Mutex |
select for timeouts, cancellation, and multiple channel operations.
Explore 500+ free tutorials across 20+ languages and frameworks.