Channels
What are Channels?
Why use them?
- They remove the need for complex synchronization structures for communication.
- They help avoid race conditions when used well.
Declaring and Creating Channels
You can declare a channel with the syntax:
var ch chan int
This defines a channel of integers, but doesn’t initialize it yet.
In Go, when you define a channel of integers, you are only declaring that the channel exists, but you are not creating an instance of it yet. To actually initialize the channel and make it usable, you need to use the make function.
To create (initialize) a channel, use the make function:
ch := make(chan int)
This creates an unbuffered channel that can be used to send and receive values of type int.
Basic Operations with Channels
Sending data
Use the syntax:
ch <- value
This sends “value” to the channel, blocking the goroutine’s execution if there is no receiving goroutine (in the case of an unbuffered channel).
Receiving data
Use the syntax:
value := <-ch
This waits for and receives a value from the channel. If no value is available, the goroutine stays blocked until a value is sent.
Simple Example
package main
import (
"fmt"
"time"
)
func main() {
// Creates and initializes the channel.
ch := make(chan int)
// Launches a goroutine to send a value.
go func() {
ch <- 42
}()
// Receives and prints the sent value.
value := <-ch
fmt.Println("Received:", value)
}
Notice that we didn’t need a sync.WaitGroup or a time.Sleep here, because using channels in Go allows direct synchronization between goroutines.
In the example, the anonymous goroutine sends the value 42 to the channel ch, and the main function waits for that value to arrive before continuing.
This happens because the send operation (ch <- 42) blocks the goroutine until another goroutine receives the value, and the receive operation (<-ch) blocks the main function until a value is sent.
This way, the channel guarantees that communication and synchronization between goroutines happen efficiently and without the need for additional synchronization mechanisms.
Unbuffered vs. Buffered Channels
In Go there are two kinds of channels, and we have already used one of them. Let’s dig into the differences between them.
Unbuffered channel
- Requires the send and receive operations to happen at the same time.
- The sending goroutine stays blocked until another goroutine is ready to receive.
- Example:
ch := make(chan int)(without specifying a size).
Buffered channel
- Allows a limited number of values to be stored without immediately blocking the send.
- The buffer is defined as the second parameter of the
makefunction. - Example:
ch := make(chan int, 5)creates a channel that can store up to 5 integers before blocking the send.
Demonstration
Imagine you have a buffered channel and you send three values in a row:
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
In this situation, the three send operations don’t block, because there is enough room in the buffer. If you try to send a fourth value, the operation will be blocked until some value is received.
Closing Channels
When to close a channel
You should close a channel when there are no more values to send. This tells the receivers that no further information will be sent.
How to close
Use the close function:
close(ch) // ch is the name of the variable where your channel is stored
Receiving after closing
When trying to receive from a closed channel, the operation returns the zero value of the type, plus an “ok” flag that indicates whether the channel is open.
Example:
value, ok := <-ch
if !ok {
fmt.Println("Channel closed, no reads available")
}
// If `ch` is a channel of integers, the variable `value` will be `0`,
// for a string it will be "", etc...
Iterating over Channels
Just like maps, arrays and slices, you can also iterate over the values of a channel.
Using the for…range loop
It lets you iterate over the values sent on a channel until it is closed.
Example:
for v := range ch {
fmt.Println("Value received:", v)
}
Complete practical example
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 5)
// Launches a goroutine that sends data and closes the channel.
go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch)
}()
// The for...range loop keeps going while the channel is open.
for value := range ch {
fmt.Println("Received:", value)
}
}
Considerations and Care with Channels
Preventing deadlocks
- Make sure that for every send there is a receiver, especially with unbuffered channels.
- When using buffered channels, avoid filling the buffer without emptying it, since that can block the sending goroutine.
Closing properly
- Only the producer (whoever sends data) should close the channel.
- Avoid closing the channel from inside a goroutine that is also reading from it, since it can cause a
panicif another producer tries to send.
Good practices
- Always test the behavior of your concurrent code to identify and fix possible blocking.
- Use
value, ok := <-chto check whether the channel was closed and handle the condition appropriately.
Exercises
-
Simple Send with an Unbuffered Channel
- Requirement: Create a channel of integers, launch a goroutine that sends the value 100 to the channel and, in main, receive and print that value.
-
Send and Receive with a Buffered Channel
- Requirement: Create a buffered channel (capacity 3), send three values in a row without causing blocking and then receive and print all the values in main.
-
Closing a Channel with Iteration
- Requirement: Launch a goroutine that sends the numbers 1 to 5 to a channel, closes the channel and, in main, iterates with a for…range loop to print all the received values.
-
Checking for a Closed Channel
- Requirement: Create a channel, send a value, close it and then, in main, use the receive form
value, ok := <-chto print the value and indicate whether the channel is still open.
- Requirement: Create a channel, send a value, close it and then, in main, use the receive form
-
Combination: Goroutine and Channel for a Calculation
- Requirement: Launch a goroutine that computes the sum of the numbers 1 to 5 and sends the result through a channel; in main, receive and print the result.
-
Combination: WaitGroup and Channel
- Requirement: Launch 2 goroutines that perform simple operations (for example, an addition and a multiplication) and send the results through a channel; use
sync.WaitGroupto synchronize the goroutines and, after they finish, receive and print the results in main. - Bonus Challenge: Can you solve this exercise without using WaitGroups?
- Requirement: Launch 2 goroutines that perform simple operations (for example, an addition and a multiplication) and send the results through a channel; use
-
Complete Example: Several Goroutines with WaitGroup and Channel
- Requirement: Create a function that launches 5 goroutines, each sending its identifier (from 1 to 5) to a channel; use a
WaitGroupto wait for all of them to finish and then iterate in main (usingfor...range) to print all the values from the channel. Remember to close the channel after the goroutines finish. - Bonus Challenge: Can you solve this exercise without using WaitGroups?
- Requirement: Create a function that launches 5 goroutines, each sending its identifier (from 1 to 5) to a channel; use a
Common mistakes with Channels
Blocking Operations on Unbuffered Channels
- Mistake: Trying to send a value to an unbuffered channel without a receiving goroutine, causing blocking (deadlock).
- Solution: Make sure there is a goroutine ready to receive the value, or use a buffered channel when appropriate.
Forgetting to Close a Channel
- Mistake: Not closing a channel when the producer finishes sending values can lead to infinite loops (for example, in a
for…range), since the receiver will keep waiting for new values. - Care: Only the producer should close the channel, and always close it after the final send.
Trying to Send Data on a Closed Channel
- Mistake: Sending values on a channel that has already been closed causes a panic, making the program stop working.
- Note: Close the channel only when you are sure no other send will happen.
Exercise solutions
So I wouldn’t have to run each exercise by hand, I used tests and testable examples.
Exercise 1:
// main.go
func Ex1() {
ch := make(chan int)
go func() {
ch <- 100
}()
fmt.Println(<-ch)
}
// main_test.go
func ExampleEx1() {
Ex1()
// output: 100
}
Exercise 2:
// main.go
func Ex2() {
ch := make(chan string, 3)
go func() {
ch <- "one"
ch <- "two"
ch <- "three"
}()
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
// main_test.go
func ExampleEx2() {
Ex2()
// output:
// one
// two
// three
}
Exercise 3:
// main.go
func Ex3() {
ch := make(chan int)
go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
}
// main_test.go
func ExampleEx3() {
Ex3()
// output:
// 1
// 2
// 3
// 4
// 5
}
Exercise 4:
// main.go
func Ex4() {
ch := make(chan float64)
go func() {
ch <- 10.0
close(ch)
}()
if v, ok := <-ch; ok {
fmt.Printf("%.1f", v)
}
}
// main_test.go
func ExampleEx4() {
Ex4()
// output:
// 10.0
}
Exercise 5:
// main.go
func Ex5() {
ch := make(chan int)
go func() {
n := 5
sum := (n * (n + 1)) / 2
ch <- sum
close(ch)
}()
fmt.Println("Sum:", <-ch)
}
// main_test.go
func ExampleEx5() {
Ex5()
// output:
// Sum: 15
}
Exercise 6:
// main.go
func Ex6() {
ch := make(chan int, 2)
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
ch <- 20 + 5
}()
wg.Add(1)
go func() {
defer wg.Done()
ch <- 10 * 10
}()
wg.Wait()
fmt.Println("First:", <-ch)
fmt.Println("Second:", <-ch)
}
// main_test.go
func ExampleEx6() {
Ex6()
// output:
// First: 100
// Second: 25
}
Exercise 7:
// main.go
func Ex7() {
const repetitions = 5
ch := make(chan int, repetitions)
wg := &sync.WaitGroup{}
wg.Add(repetitions)
for i := range repetitions {
go func(i int) {
defer wg.Done()
ch <- i + 1
}(i)
}
wg.Wait()
close(ch)
for i := range ch {
fmt.Println("Goroutine:", i)
}
}
// main_test.go
func TestEx7(t *testing.T) {
out := captureOutput(Ex7)
expectedOutputs := []string{
"Goroutine: 1",
"Goroutine: 2",
"Goroutine: 3",
"Goroutine: 4",
"Goroutine: 5",
}
for _, expected := range expectedOutputs {
if !strings.Contains(out, expected) {
t.Fatal("Output not generated: ", expected, " got: ", out)
}
}
}
// main_test.go
func captureOutput(f func()) string {
orig := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
f()
os.Stdout = orig
w.Close()
out, _ := io.ReadAll(r)
return string(out)
}
Next steps!
Next we will cover Select Statements! Stay tuned for the coming days!