Channels

Like a river flows, surely to the sea

4-minute read
Made by ChickenFryBytes Studios
Table of Contents

We can use channels in order to send information among concurrent goroutines. They can be thought of as pipes in which we pass data around. A new channel is created using the syntax:


make(chan <dataType>)
// e.g. a channel for passing strings
myChannel := make(chan string)

We can then send values through the channel using the syntax:


myChannel <- "this is my message to you"

Receiving values from a channel

In order to get a value in a channel, we use the syntax:


// store value from channel into variable myMessage
myMessage := <-myChannel

Consider the following code:

package main

import "fmt"

func main() {
	littleBirdie := make(chan string)

	myMessages := []string{
		"Lester is ok",
		"Nana is at least 60 now",
	}

	for _, message := range myMessages {
		go func() {
			littleBirdie <- message
		}()
	}

	// get first message
	message := <-littleBirdie
	// print first message
	fmt.Println(message)

	// print second message
	fmt.Println(<-littleBirdie)
}
channels.go
Copy

The send and receive operations will block until both sender and receiver are ready. Note how we do not need to use synchronization primitives like wait groups to ensure that the program waits for each goroutine to complete before terminating.

Channels are unbuffered

Channels are, by default, unbuffered meaning they only accept sends (chan <-) once there is a corresponding receive (<- chan) in the code. They will block execution until the value sent is received by a goroutine.

A deadlock occurs when there is a send and receive operation in the same goroutine:

package main

import "fmt"

func main() {
	bits := make(chan int)
	bits <- 1

	fmt.Println("Sent a bit of 1")
	fmt.Println("Received", <-bits)
}
channels-deadlock.go
Copy

Deadlocks can also occur if the receive operations exceed the number of values passed into the channel:

package main

import "fmt"

func main() {
	bits := make(chan int)
	go func() {
		bits <- 1
		bits <- 0
	}()

	fmt.Println("Sent a bit of 1")
	fmt.Println("Received", <-bits)
	fmt.Println("Received", <-bits)

	// asking for too many values
	fmt.Println("Received", <-bits)
}
channels-deadlock-2.go
Copy

Channel buffering

Buffered channels can accept a limited number of values without needing a corresponding receiver for these values. The make command is used in the usual fashion but with a second parameter being the maximum number of values that can be sent in the channel without needing the receiver:

package main

import "fmt"

func main() {
	messages := make(chan string, 5)

	// send messages
	messages <- "Hello"
	messages <- "World!"
	messages <- "How"
	messages <- "are"
	messages <- "you?"

	// receive messages
	fmt.Println(<-messages)
	fmt.Println(<-messages)
	fmt.Println(<-messages)
	fmt.Println(<-messages)
	fmt.Println(<-messages)
}
channels-buffered.go
Copy

Of course, a deadlock results if we try to receive too many values (try duplicating the fmt.Println statement and rerun the code).

Channel synchronization

The execution of code across goroutines can be synchronized via channels. Channels can be used to block goroutines. The following code terminates after the goroutine sending cows closes the channel. Most of the sheep won’t sent sent:

package main

import (
	"fmt"
	"strconv"
	"time"
)

func main() {
	c := make(chan string, 5)

	// send 100 sheep
	go count(100, "sheep", c)
	// send only 4 cows
	go count(4, "cow", c)

	// receive values from c channel,
	// blocking the current (main) goroutine until it is closed by the sender
	for msg := range c {
		fmt.Println(msg)
	}
}

func count(number int, thing string, c chan string) {
	for i := 1; i <= number; i++ {
		// send value via c channel to main goroutine
		c <- strconv.Itoa(i) + " " + thing
		time.Sleep(time.Millisecond * 500)
	}

	// close channel
	close(c)
}
channels-racing-to-count.go
Copy

Consider the worker pattern:

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(name string, jobs <-chan string) {
	// will block current go routine and wait for jobs
	// until jobs channel is closed
	for job := range jobs {
		fmt.Println(name, "begins to eat", job)
		time.Sleep(time.Second * 3)
		fmt.Println(name, "ate the", job)
	}
}

func main() {
	names := []string{"John", "James", "Mary"}
	mealsToEat := []string{"banana", "hotdog", "chicken"}
	mealsChannel := make(chan string, 10)

	var wg sync.WaitGroup
	// spawn a worker in a go routine for each person
	for _, name := range names {
		wg.Go(func() {
			worker(name, mealsChannel)
		})
	}

	// send meals into meals channel
	for _, meal := range mealsToEat {
		mealsChannel <- meal
	}
	// close channel to unblock workers when channel is empty
	close(mealsChannel)
	fmt.Println("closed meals channel")

	wg.Wait()
}
channels-worker-pattern.go
Copy

Channel directions

Created using natural intelligence


Like our content? Support us via Donations