Channels
Do not communicate by sharing memory; instead, share memory by communicating.
Channels are a powerful concurrency primitive that allows communication and synchronization between goroutines.
You can create a channel using the make
function.
By default channels are unbuffered, meaning that they will only accept sends (chan <-
) if there is a corresponding receive (<- chan
) ready to receive the sent value.
ch := make(chan int) // Creates an unbuffered channel of type int
ch2 := make(chan int, 3) // Buffered channel with a capacity of 3
Buffering
Buffered channels accept a limited number of values without a corresponding receiver for those values.
package main
import "fmt"
func main() {
messages := make(chan string, 2)
messages <- "lite"
messages <- "rank"
fmt.Println(<-messages)
fmt.Println(<-messages)
}
Synchronization
package main
import (
"fmt"
"time"
)
func worker(done chan int) {
fmt.Print("working...")
time.Sleep(time.Second)
fmt.Println("done")
done <- 1
}
func main() {
done := make(chan int, 1)
go worker(done)
<-done
}
The worker
function simulates some work and notifies when it's done by sending a value to the done
channel.
Directions
Channels can have directions, which means you can specify whether a channel is meant to only send or only receive values.
This is a form of type-checking at the language level to prevent misuse of channels. Channel directions are declared using the <-
operator.
Send-only Channel
package main
import "fmt"
func sendData(sendCh chan<- int) {
sendCh <- 42
}
func main() {
ch := make(chan int)
go sendData(ch)
fmt.Println("Data sent successfully")
}
Receive-only Channel
package main
import "fmt"
func receiveData(receiveCh <-chan int) {
data := <-receiveCh
fmt.Println("Received data:", data)
}
func main() {
ch := make(chan int)
go receiveData(ch)
fmt.Println("Data received successfully")
}
Closing
You can close a channel to signal that no more values will be sent on it. Closing a channel is useful for notifying receivers that they should stop waiting for new values.
package main
import (
"fmt"
"time"
)
func produceData(dataCh chan<- int) {
for i := 1; i <= 5; i++ {
dataCh <- i
}
close(dataCh)
}
func consumeData(dataCh <-chan int, done chan<- struct{}) {
for {
// The 'ok' value will be false if the channel is closed.
data, ok := <-dataCh
if !ok {
fmt.Println("Channel closed. No more data.")
close(done) // Signal that consumption is done
return
}
fmt.Println("Received data:", data)
time.Sleep(time.Second)
}
}
func main() {
dataCh := make(chan int)
done := make(chan struct{})
go produceData(dataCh)
go consumeData(dataCh, done)
// Block until the 'done' channel is closed, indicating that consumption is complete.
<-done
fmt.Println("Main goroutine exiting.")
}
Ranging Over
The for ... range
loop is commonly used with channels in Go to iterate over values until the channel is closed.
package main
import (
"fmt"
"time"
)
func produceData(dataCh chan<- int) {
for i := 1; i <= 5; i++ {
dataCh <- i
}
close(dataCh)
}
func consumeData(dataCh <-chan int, done chan<- struct{}) {
for data := range dataCh {
fmt.Println("Received data:", data)
time.Sleep(time.Second) // Simulate some processing
}
// Channel is closed, signal that consumption is done
close(done)
}
func main() {
dataCh := make(chan int)
done := make(chan struct{})
go produceData(dataCh)
go consumeData(dataCh, done)
// Block until the 'done' channel is closed, indicating that consumption is complete.
<-done
fmt.Println("Main goroutine exiting.")
}
Non-Blocking Operations
You can perform non-blocking operations on channels using the select
statement.
The select
statement allows you to wait on multiple communication operations simultaneously and proceed with the first one that can proceed.
package main
import "fmt"
func main() {
dataCh := make(chan int, 1)
dataCh <- 58
// Try sending a value to the channel without blocking.
select {
case dataCh <- 42:
fmt.Println("Data sent successfully.")
default:
fmt.Println("Unable to send data. Channel is full.")
}
// => Unable to send data. Channel is full.
// Try receiving a value from the channel without blocking.
select {
case data := <-dataCh:
fmt.Println("Received data:", data)
default:
fmt.Println("No data available. Channel is empty.")
}
// => Received data: 58
close(dataCh)
}
Worker Pools
A worker pool is a common concurrency pattern where a fixed number of worker goroutines are used to process tasks from a queue. Channels are often used for communication between the main goroutine (which produces tasks) and the worker goroutines (which consume and process tasks).
package main
import "fmt"
func worker(id int, jobs <-chan int, results chan<- int, done chan<- struct{}) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
// Signal that the worker has finished its work
done <- struct{}{}
}
func main() {
const numJobs = 10
const numWorkers = 3
// Create channels for communication between main and worker goroutines.
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
done := make(chan struct{}, numWorkers)
for i := 1; i <= numWorkers; i++ {
go worker(i, jobs, results, done)
}
// Produce jobs and send them to the jobs channel.
go func() {
for i := 1; i <= numJobs; i++ {
jobs <- i
}
close(jobs)
}()
// Wait for all workers to finish.
for i := 1; i <= numWorkers; i++ {
<-done
}
close(results)
for result := range results {
fmt.Printf("Result: %d\n", result)
}
}
Code Challenge
Write a Go program that counts the occurrences of words in a string concurrently. The program should use goroutines to process the input string concurrently and channels to communicate the results.