Go Channels Complete Tutorial Explained in Layman's Terms


GO

Reviewer: Deepak Prasad

Getting started with GO Channels

In Go programming, you may want goroutines to communicate with one another. Go channels are responsible for allowing that kind of communication to take place. Therefore, go channels are conduit pipes that are typed through which values are sent and received. On the other hand, goroutines are light weight threads that are managed by the main go thread. For information about goroutine, visit the goroutine tutorial. Go has two types of channels that we will discuss later in this tutorial. They are buffered and unbuffered channels.

 

Why use channels in golang?

  1. Go channels make asynchronous programming easy and fun to use.
  2. Channels provide mechanisms for concurrent execution of functions to communicate by sending and receiving values of a specific element type.

 

Creating a Go channel

It is easy to create Go channels. You only need to define a channel, channel type using the make function and assign that to a variable. Just like any other types like structs, maps and slices, you need to create a channel before you can use it as shown below.

Syntax

package main
 
func main() {
   myChannel := make(chan datatype)
}

Here is a more descriptive syntax to create a go channel

Go Channels Complete Tutorial Explained in Layman's Terms

 

How channels work (sending and receiving data)?

In Go channels, it is very easy to send and receive values to and from a Go channel. Go provides an operator used to show the direction of flow to and from a channel. This operator is <- , a less than and minus sign. It is more like an arrow that will always point to the direction of value flow.  Below is an example of sending a value into a channel.

myChannel <- "Hello world"

In the above example, we are sending the string “Hello world” string into the mychannel channels. It is worth mentioning that the channel only accepts values that are of the same type as the channel.We can also receive values from a channel and store the value in a variable as shown below.

value := <-myChannel

In the above example, the arrow points away from the channel, to indicate that messages are coming from the channel.

Example

package main
 
import "fmt"
 
func main() {
 
   linkChannel := make(chan string)
   go func() {
       links := []string{"https://www.golinuxcloud.com/", "https://www.tesla.com/", "https://www.google.com/"}
       for _, link := range links {
           linkChannel <- link
       }
   }()
 
   link1 := <-linkChannel
   link2 := <-linkChannel
   link3 := <-linkChannel
 
   fmt.Println(link1)
   fmt.Println(link2)
   fmt.Println(link3)
}

Explanation

In the above example, we define a go channel of type string using the line linkChannel := make(chan string). Values of string type are the only acceptable values that can go through the channel. We then define an anonymous goroutine that loops through an  array of links. For each loop we send a link into the channel using linkChannel <- link. The goroutine will run in the background simultaneously with the main thread.

One important thing to note is that, the main thread(main goroutine) has to wait for messages to come from the channel. This means that receiving values from channels is blocking. Channels will allow messages to go through it only when the receiver is ready to receive messages from it.

Output

$ go run main.go
https://www.golinuxcloud.com/
https://www.tesla.com/
https://www.google.com/

 

Properly closing a channel

Channels support a third operation, close, which sets a flag indicating that no more values will ever be sent on this channel; subsequent attempts to send will panic. Receive operations on a closed channel yield the values that have been sent until no more values are left; any receive operations thereafter complete immediately and yield the zero value of the channel’s element type.

To close a channel, we call the built-in close function:

close(ch)

Example

package main
 
import "fmt"
 
func main() {
 
   linkChannel := make(chan string)
   go ping(linkChannel)
 
   link1 := <-linkChannel
   link2 := <-linkChannel
   link3 := <-linkChannel
 
   fmt.Println(link1)
   fmt.Println(link2)
   fmt.Println(link3)
}
 
func ping(c chan string) {
   links := []string{"https://www.golinuxcloud.com/", "https://www.tesla.com/", "https://www.google.com/"}
   for _, link := range links {
       c <- link
   }
   close(c)
}

Explanation

We define a ping() function that runs as a goroutine. ping() function takes a channel of string as an argument. It loops through an array of links and sends these links to the channel. When the loop is done, we exit the for loop and close the channel using the close() syntax. Closing a channel means no more values can be sent into the channel.An attempt to send values into a closed channel will result in a panic with messages that say “send on closed channel”.

Output

$ go run main.go
https://www.golinuxcloud.com/
https://www.tesla.com/
https://www.google.com/

 

How to check if channel is closed?

It's possible to check if a channel is closed or not using the below syntax.

package main
 
import "fmt"
 
func main() {
 
   linkChannel := make(chan string)
   go ping(linkChannel)
 
   for {
       _, ok := <-linkChannel
       if ok {
           fmt.Println("Channel Open")
       } else {
           fmt.Println("Channels closed")
           break
       }
   }
}
 
func ping(c chan string) {
   links := []string{"https://www.golinuxcloud.com/", "https://www.tesla.com/", "https://www.google.com/"}
   for _, link := range links {
       c <- link
   }
   close(c)
}

Explanation

In the above example, we check if a channel is closed or is open, using the syntax _, ok := <-linkChannel. We really don't care about the first returned value , therefore we use the underscore. The ok variable is a boolean type and turns true when the channel is open and turns false when the channel is closed.

Output

$ go run main.go
Channel Open
Channel Open
Channel Open
Channels closed

 

Different types of go channels

Unbuffered channels

Unbuffered channels are channels that can only accept sending (chan <- ) of values through them only when we have a corresponding receiver (<- chan) that will receive the sent values. Here we have two functions — sendData() and getData():

package main

import (
	"fmt"
	"time"
)

// ---send data into a channel---
func sendData(ch chan string) {
	fmt.Println("Sending a string into channel…")

	ch <- "Hello"
	fmt.Println("String has been retrieved from channel…")
}

// ---getting data from the channel---
func getData(ch chan string) {
	time.Sleep(2 * time.Second)
	fmt.Println("String retrieved from channel:", <-ch)
}

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

	go sendData(ch)
	go getData(ch)

	fmt.Scanln()
}

Explanation:

In the main() function, you first create a channel using the make() function, ch, with the type of channel specified (string). This means that the channel can only contain values of type string. You then call the sendData() and getData() functions as goroutines. In the sendData() function, you first print out the sentence Sending a string into channel…

At the same time, we're also running the getData() function. Here, with getData(), you're trying to receive a value from the channel. Because there is currently no value in the channel (it won’t have any value in it until two seconds later), the getData() function will block. The moment a value is available in the channel, the getData() function will unblock and retrieve the value from the channel.

In the sendData() function, you try to print a sentence immediately after sending a value into the channel. In the getData() function, you insert a two-second delay before you retrieve the value from the channel.

Output:

$ go run main.go 
Sending a string into channel…
String retrieved from channel: Hello
String has been retrieved from channel…

 

Buffered channels

On the other hand, buffered channels  only accept a limited number of values without a corresponding receiver of those values. Provide the buffer size/length/capacity as the second argument to the make function to initialize a buffered channel. Buffered channels can only block when the buffer is full. Therefore sending on a buffered channel will block the channel when the channel is full and the receiver will block when the buffer is empty.

 

Buffered Channel Syntax

The size of the buffer is specified as an argument to the make function

Go Channels Complete Tutorial Explained in Layman's Terms

 

Example

package main
 
import (
   "fmt"
)
 
func main() {
 
   linkChannel := make(chan string, 5)
   go ping(linkChannel)
 
   fmt.Println(<-linkChannel)
   fmt.Println(<-linkChannel)
   fmt.Println(<-linkChannel)
 
}
 
func ping(c chan string) {
   links := []string{"https://www.golinuxcloud.com/", "https://www.tesla.com/", "https://www.google.com/"}
   for _, link := range links {
       c <- link
   }
}

Explanation

In the above example we  define a goroutine that loops through links and sends them to the buffered linkChannnel channel. In the main thread(main routine), we receive the values(fmt.Println(<-linkChannel)) being sent from the ping() goroutine and print the values out in the terminal.

Please note that buffered channels will panic or return an error whenever you try to add more values than the buffer size. Also the receiver will cause a panic if we try to receive(<-linkChannel) from an empty channel.

Output

$ go run main.go
https://www.golinuxcloud.com/
https://www.tesla.com/
https://www.google.com/

 

How different channels work?

Channels have many different states, each with a different behavior when reading, writing, or closing.

  Unbuffered, open Unbuffered, closed Buffered, open Buffered, closed Nil
Read Pause until something is written Return zero value (use comma ok to see if closed) Pause if buffer is empty Return a remaining value in the buffer. If the buffer is empty, return zero value (use comma ok to see if closed) Hang forever
Write Pause until something is read PANIC Pause if buffer is full PANIC Hang forever
Close Works PANIC Works, remaining values still there PANIC PANIC

 

Go channel select statement

The select statement in Go channels is used to wait on multiple channel operations like receiving and sending values. The select statement waits for simultaneous send and receive operations. Below are the characteristics of a select statement working with channels.

  1. select statement is a  blocking statement. This means that one case in the select statement will have to wait until one of the operations becomes unblocked.
  2. If several cases can proceed, a single one case will be chosen to execute at random.
  3. The default case will be executed if all cases are blocked.

Select statement syntax

select {
   case <-ch1:
       fmt.Println("Getting data from channel one")
   case <-ch2:
       fmt.Println("Getting data from channel two")
   default:
       fmt.Println("Run if all cases are blocked")
   }
}

Example

package main
 
import (
   "fmt"
   "time"
)
 
func goroutine1(ch1 chan string) {
   time.Sleep(time.Second)
   for i := 0; i < 3; i++ {
       ch1 <- fmt.Sprintf("%d ==> Channel 1 message", i)
   }
}
 
func goroutine2(ch2 chan string) {
   for i := 0; i < 3; i++ {
       ch2 <- fmt.Sprintf("%d ==> Channel 2 message", i)
   }
 
}
 
func main() {
   ch1 := make(chan string)
   ch2 := make(chan string)
 
   go goroutine1(ch1)
   go goroutine2(ch2)
 
   for {
       time.Sleep(time.Second * 3)
       select {
 
       case value1 := <-ch1:
           fmt.Println(value1)
       case value2 := <-ch2:
           fmt.Println(value2)
       default:
           fmt.Println("All channels are blocking")
       }
   }
 
}

Explanation

In our select example above, we define two goroutines that loop through integers and send a string value to the channel after every iteration. The first goroutine1 sleeps for a second before executing while goroutine2 starts executing immediately after being called. This is to show an example of a function that takes much longer time than the other to run. We then declare and call two goroutines to start sending values into their respective channels. In order to have enough time to receive those values, we use an infinite loop that has the select statement. The select statement sleeps for 3 seconds to give the channels time to start reporting value in the main thread. The select statement will then start executing cases depending on which channel is ready to receive values as shown in the output. Another thing to note is that the default case will be called when channels finish receiving values (start blocking).

Output

$ go run main.go
0 ==> Channel 1 message
0 ==> Channel 2 message
1 ==> Channel 1 message
1 ==> Channel 2 message
2 ==> Channel 1 message
2 ==> Channel 2 message
All channels are blocking

 

Some go channel examples

Example-1: How to count values in a channel

To get the number of values in a Go channel, use the built in function len().len() returns the number of values in a buffered channel.

Example

package main
 
import "fmt"
 
func main() {
   ch := make(chan int, 5)
   for i := 0; i < 3; i++ {
       ch <- i
   }
 
   fmt.Printf("Number of values in a channel: %d \n", len(ch))
}

Output

$ go run main.go
Number of values in a channel: 3

 

Example-2: Sending struct over  a channel

Many examples in this tutorial, we have used Go channels for type string and int. In this section we will explore using channels of type struct. Struct in a Go is a collection of fields, hence giving a chance to send and receive different data types across channels.

Example

package main
 
import "fmt"
 
type HTTPResponse struct {
   statusCode int
   body       string
}
 
func ping(c chan HTTPResponse, responses []HTTPResponse) {
   for i := 0; i < len(responses); i++ {
       c <- responses[i]
   }
   close(c)
}
 
func main() {
 
   response := []HTTPResponse{
       {
           statusCode: 200,
           body:       "Resource OK",
       },
       {
           statusCode: 201,
           body:       "Resource Created",
       },
       {
           statusCode: 404,
           body:       "Resource Not Found",
       },
   }
   ch := make(chan HTTPResponse, 5)
 
   go ping(ch, response)
 
   fmt.Println(<-ch)
   fmt.Println(<-ch)
   fmt.Println(<-ch)
}

Output

$ go run main.go
{200 Resource OK}
{201 Resource Created}
{404 Resource Not Found}

 

Example-3: Iterating through channels

We can use range keyword to iterate through a channel:

Example

package main

import (
	"fmt"
	"time"
)

func fib(n int, c chan int) {
	a, b := 1, 1
	for i := 0; i < n; i++ {
		c <- a // blocked until value is received from channel
		a, b = b, a+b
		time.Sleep(1 * time.Second)
	}
	close(c) // close the channel
}

func main() {
	c := make(chan int)
	go fib(10, c)
	for i := range c { // read from channel until channel is closed
		fmt.Println(i)
	}
}

Explanation:

In this example, you have a function named fib() that takes in two arguments — the number of elements to generate for the Fibonacci sequence, and the channel to store the numbers in. Each number of the Fibonacci sequence is calculated and then sent into the channel. I've added a delay of one second for each number to simulate some delay. As each Fibonacci number is being generated and inserted into the channel, the fib() function blocks until the value is retrieved from the channel. After generating all the required Fibonacci numbers, you close the channel (using the close() function) to indicate that the channel is no longer accepting values.

In the main() function, we first create an instance of the channel using the make() function, and then proceed to call the fib() function as a goroutine — generating the first ten Fibonacci numbers.

You use the range keyword on the c channel to keep on reading values until the channel is closed.

Output:

$ go run main.go 
1
1
2
3
5
8
13
21
34
55

 

Summary

Channels used together with goroutines are a powerful tool. Channels help developers to write goroutines that send and receive messages easily, hence making asynchronous programming easy and fun. In this tutorial we have explored what channels are and how they are being used.

 

References

goroutine concurrency
golang channel

 

Related Keywords: why use channels in golang, go channel select, go channel close, golang read from channel, golang count items in channel, golang channel blocking, golang channel types (unbuffered channel, bufferred channel), golang channel with multiple values, golang channel limit, golang send struct over channel, Why go channels are bad

 

Antony Shikubu

Antony Shikubu

He is highly skilled software developer with expertise in Python, Golang, and AWS cloud services. Skilled in building scalable solutions, he specializes in Django, Flask, Pandas, and NumPy for web apps and data processing, ensuring robust and maintainable code for diverse projects. You can reach out to him on his LinkedIn profile.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment