Skip to main content
Channels are the pipes that connect concurrent goroutines. They allow you to send values from one goroutine and receive them in another, providing a safe way to communicate and synchronize.

Creating Channels

Channels are created using make() and are typed by the values they convey:
package main

import "fmt"

func main() {
	// Create a channel of strings
	messages := make(chan string)

	// Send a value into the channel from a goroutine
	go func() { messages <- "ping" }()

	// Receive the value from the channel
	msg := <-messages
	fmt.Println(msg)  // Output: ping
}

Channel Syntax

Send Operation

channel <- value
Sends value into channel. This operation blocks until a receiver is ready.

Receive Operation

value := <-channel
Receives a value from channel. This operation blocks until a value is available.
By default, sends and receives block until both the sender and receiver are ready. This property is called synchronization.

Blocking Behavior

Channels synchronize execution between goroutines:
messages := make(chan string)

go func() {
	time.Sleep(1 * time.Second)
	messages <- "ping"
}()

// This line blocks until the goroutine sends a value
msg := <-messages
fmt.Println(msg)
The receive operation <-messages waits for the send operation to complete.
Sending or receiving on an unbuffered channel from the same goroutine without another goroutine will cause a deadlock.

Common Deadlock Example

// DEADLOCK! Don't do this:
ch := make(chan string)
ch <- "value"  // Blocks forever waiting for a receiver
msg := <-ch    // Never reached

Channel Types

Channels can convey any type:
intChannel := make(chan int)
stringChannel := make(chan string)
structChannel := make(chan MyStruct)
errorChannel := make(chan error)

Practical Example: Goroutine Communication

func fetchData(url string, results chan<- string) {
	// Simulate fetching data
	data := "data from " + url
	results <- data
}

func main() {
	results := make(chan string)
	
	go fetchData("https://api.example.com", results)
	
	// Wait for and receive the result
	data := <-results
	fmt.Println(data)
}

Channel Operations Summary

OperationSyntaxBlocks Until
Sendch <- vReceiver ready
Receivev := <-chSender ready
Createmake(chan T)N/A
Closeclose(ch)N/A
Channels are first-class values - you can pass them as function parameters, return them from functions, and store them in data structures.

Receive with Status Check

value, ok := <-channel
if ok {
	fmt.Println("Received:", value)
} else {
	fmt.Println("Channel closed")
}
The second return value ok is false if the channel is closed and empty.

Best Practices

  1. Use channels for communication - Share memory by communicating, don’t communicate by sharing memory
  2. Close channels from the sender - Only the sender should close a channel
  3. Check channel status - Use the two-value receive form when necessary
  4. Prefer unbuffered channels - Unless you have a specific need for buffering

Common Patterns

Signal Channel

done := make(chan bool)
go func() {
	// Do work
	done <- true
}()
<-done  // Wait for completion

Pipeline

func gen(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		for _, n := range nums {
			out <- n
		}
		close(out)
	}()
	return out
}

Build docs developers (and LLMs) love