Go Channels
Introduction
In Go's concurrency model, channels are a fundamental concept that allow goroutines to communicate with each other and synchronize their execution. Think of channels as pipes that connect concurrent goroutines, allowing them to send and receive values with proper synchronization.
Channels solve a common problem in concurrent programming: safely sharing data between multiple threads of execution. Instead of sharing memory and using locks, Go encourages communicating by sharing channels.
"Don't communicate by sharing memory; share memory by communicating." - Go proverb
What Are Channels?
Channels in Go are typed conduits that allow you to send and receive values between goroutines. They act as both communication and synchronization mechanisms, ensuring that data is safely passed between concurrent parts of your program.
Channel Creation
You create a channel using the built-in make
function:
// Create an unbuffered channel of integers
ch := make(chan int)
// Create a buffered channel of strings with capacity 10
bufferedCh := make(chan string, 10)
Types of Channels
Go provides two types of channels:
- Unbuffered channels: Send operations block until there's a corresponding receive.
- Buffered channels: Have a capacity and only block when the buffer is full.
Let's explore both types with examples.
Unbuffered Channels
An unbuffered channel requires both the sender and receiver to be ready at the same time for the communication to take place. This creates a perfect synchronization point between goroutines.
package main
import (
"fmt"
"time"
)
func main() {
// Create an unbuffered channel
ch := make(chan string)
// Start a goroutine that sends a message
go func() {
fmt.Println("Goroutine: Sending message...")
time.Sleep(2 * time.Second) // Simulate work
ch <- "Hello from goroutine!" // This blocks until main goroutine receives
fmt.Println("Goroutine: Message sent!")
}()
fmt.Println("Main: Waiting for message...")
msg := <-ch // This blocks until the goroutine sends
fmt.Println("Main: Received message:", msg)
}
Output:
Main: Waiting for message...
Goroutine: Sending message...
Goroutine: Message sent!
Main: Received message: Hello from goroutine!
In this example, the main goroutine and the anonymous goroutine synchronize perfectly - the send operation (ch <- "Hello from goroutine!"
) blocks until the receive operation (msg := <-ch
) is ready.
Buffered Channels
Buffered channels have a capacity defined when they're created. Sends to a buffered channel only block when the buffer is full, and receives only block when the buffer is empty.
package main
import (
"fmt"
"time"
)
func main() {
// Create a buffered channel with capacity 2
ch := make(chan string, 2)
// Send 2 messages immediately (won't block because buffer has space)
ch <- "First message"
ch <- "Second message"
fmt.Println("Sent two messages without blocking")
// This would block until space is available
// Uncomment to see blocking behavior:
// ch <- "Third message"
// fmt.Println("This line won't be reached until someone receives a message")
// Receive messages
fmt.Println("Received:", <-ch)
fmt.Println("Received:", <-ch)
}
Output:
Sent two messages without blocking
Received: First message
Received: Second message
Buffered channels are useful when:
- You know the exact number of goroutines you've launched
- You need to limit the amount of work that's queued up
- The sender and receiver work at different speeds
Channel Direction
Channels can be defined with a direction, restricting them to either sending or receiving operations. This is useful when passing channels to functions.
package main
import "fmt"
// This function can only receive from the channel
func receiver(ch <-chan string) {
msg := <-ch
fmt.Println("Received:", msg)
}
// This function can only send to the channel
func sender(ch chan<- string) {
ch <- "Hello, channel!"
}
func main() {
ch := make(chan string)
go sender(ch)
receiver(ch)
}
Output:
Received: Hello, channel!
Closing Channels
When you're done sending values on a channel, you can close it to indicate no more values will be sent. Receivers can test whether a channel has been closed.
package main
import "fmt"
func main() {
jobs := make(chan int, 5)
done := make(chan bool)
// Consumer goroutine
go func() {
for {
j, more := <-jobs
if more {
fmt.Println("Received job", j)
} else {
fmt.Println("All jobs received")
done <- true
return
}
}
}()
// Send 3 jobs
for j := 1; j <= 3; j++ {
jobs <- j
fmt.Println("Sent job", j)
}
close(jobs)
fmt.Println("Sent all jobs")
// Wait for all jobs to be processed
<-done
}
Output:
Sent job 1
Sent job 2
Sent job 3
Sent all jobs
Received job 1
Received job 2
Received job 3
All jobs received
Ranging Over Channels
A cleaner way to receive values from a channel until it's closed is to use a for range
loop:
package main
import "fmt"
func main() {
ch := make(chan int, 3)
// Send values
ch <- 1
ch <- 2
ch <- 3
close(ch)
// Receive values using range
for num := range ch {
fmt.Println("Received:", num)
}
}
Output:
Received: 1
Received: 2
Received: 3
Select Statement
The select
statement lets a goroutine wait on multiple channel operations. It's similar to a switch
statement but for channels.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// Send on ch1 after 1 second
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Message from channel 1"
}()
// Send on ch2 after 2 seconds
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Message from channel 2"
}()
// Use select to await both channels
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
}
}
}
Output:
Received: Message from channel 1
Received: Message from channel 2
The select
statement blocks until one of its cases can run, then it executes that case. If multiple cases are ready, it chooses one at random.
Timeouts with Select
You can implement timeouts using the select
statement and time.After
:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "Response after 2 seconds"
}()
select {
case response := <-ch:
fmt.Println(response)
case <-time.After(1 * time.Second):
fmt.Println("Timeout: operation took too long")
}
}
Output:
Timeout: operation took too long
Practical Example: Worker Pool
Let's create a worker pool pattern using goroutines and channels, which is a common concurrency pattern in Go:
If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)