How to use Go Channels: The Complete Guide
How to use Channels in Go

How to use Go Channels: The Complete Guide

Dead Simple Chat Team

Table of Contents

Dead Simple Chat offers prebuilt Chat that can be added in minutes to any app or website. Can be completely customized and offers powerful API and SDK.

In this blog post we will learn what Go Channels are and what a Goroutine is and how using Go Channels and Goroutines we can create programs that support concurrency and learn about the tools available to us in Go's concurrency model.

What is a Goroutine?

Goroutine is a core concept in doing concurrent programming in Go. Each individual Goroutine is a lightweight thread managed by the Go runtime.

Goroutine allows us to execute long routines concurrently. Suppose you have a program that sends 10 HTTP requests, if do not use any concurrency in your program then, your application will send HTTP requests one after another.  

The program will wait for the response from one HTTP request before sending another.

This is called blocking for I/O operations. With the help of Goroutines, you can launch 10 concurrent HTTP requests instead of waiting for one request to finish and then sending another request.

This massively improves the performance of your application. But be sure to write your program serially first and benchmark it before introducing concurrency.

Because you will only benefit from concurrency if your concurrent routines can execute independently from each other.

Chat API Trusted by world’s biggest corporations | DeadSimpleChat
Chat API and SDk that supports 10 Million Concurrent Users. Features like Pre-Built turn key Chat Solution, Chat API’s, Customization, Moderation, Q&A, Language Translation.

If we consider our 10 HTTP requests example, if the requests do not depend upon each other then we will benefit from concurrency, the response from one request is required in the next request then there will be no benefit of concurrency.

As Goroutines are managed by the Go runtime and not the operating system, hence you can launch hundreds or even thousands of Goroutines, whereas launching thousands of threads would result in a huge overhead.

Any function in go can be launched as a goroutine by placing the go keyword before invoking the function.

Doing this will launch the function in its own lightweight thread.

Consider the following example:

package main

import (
	"fmt"
	"time"
)

func sayHello(name string) {
	fmt.Println("Hello", name)
	time.Sleep(time.Second * 1)
}

func main()  {
	start := time.Now()

	sayHello("Steve")
	sayHello("Mike")

	elapsed := time.Since(start)

	fmt.Println("It took", elapsed)
}

In the above code, we have created a function called as sayHello it accepts a String and waits for 1 second. Then in the main method, we are calling the function twice once with the parameter "Steve" and the second time with "Mike".

If we run this code, it will execute sequentially, it first prints Steve and waits for 1 second then it will print Mike and wait for another second and then exits.

It took roughly 2 seconds to execute our code, as we added a 1-second wait, and the method was called twice and after each invocation, it waited 1 second.

Now we will Edit our program and launch the function call to the sayHello method as a Goroutine.

To do this we just need to add the keyword go before invoking the method

package main

import (
	"fmt"
	"time"
)

func sayHello(name string) {
	fmt.Println("Hello", name)
	time.Sleep(time.Second * 1)
}

func main()  {
	start := time.Now()

	go sayHello("Steve")
	sayHello("Mike")

	elapsed := time.Since(start)

	fmt.Println("It took", elapsed)
}

Now if we run this code, it will launch the first function call as a goroutine and execute it concurrently, so we will see the message 'Hello Mike' first and then we will see the message 'Hello Steve'.

And if you see the execution time now, it took just 1 second for our code to execute, because our goroutine was executed concurrently with the second function call happening in the main thread.

Chat API Trusted by world’s biggest corporations | DeadSimpleChat
Chat API and SDk that supports 10 Million Concurrent Users. Features like Pre-Built turn key Chat Solution, Chat API’s, Customization, Moderation, Q&A, Language Translation.

What is a Go Channel?

Typically concurrent programs use a global shared state or database to synchronise data, which gets difficult to maintain.

Channels solve this problem by eliminating global data sharing.

Goroutines communicate using channels. It is a communication pipe between goroutines and allows sending and receiving data between goroutines.

Creating a Go Channel

A Channel can be created using the make function. When creating a channel you need to specify its type, it can be a built-in type or struct.

ch := make(chan int)

In the above code snippet we have created a channel of type, this channel can only send and receive integer data.

You can use <- the operator to interact with the channel.

Reading from a Channel

To read data from the channel put the operator <- before the channel name like the code snipped below.

value := <- ch // Read data from the channel

In the above code snippet we are reading the data from the channel and writing it to the value variable.  

Writing to the Channel

To write data to the channel put the <- operator after the channel name and then write the value you want to write to the channel.

ch <- value // Write data to the channel

In the above example, we are writing the variable value to the channel.

ch <- 10

We are writing the integer 10 to the channel.

Channel Example

Now let's create a very simple channel and demonstrate the process of creating a channel, writing to the channel and reading from the channel.

package main

import (
	"fmt"
	"math/rand"
)

func makeArray(length int) []int {
	var array = []int{}

	for i := 0; i < length; i++ {
		array = append(array, rand.Intn(10))
	}

	return array;
}

func main() {

	ch := make(chan []int)
	go func ()  {
		randomArray := makeArray(10)
		ch<- randomArray
	}()

	go func ()  {
		randomArray := makeArray(10)
		ch<- randomArray
	}()

	val1 := <- ch
	val2 := <- ch

	fmt.Println(val1, val2)
}

In the above code, we have created a method called as makeArray it makes an array with random numbers of the specified length.

As we have learned any function can be launched as a goroutine by writing the go keyword, but it is a best practice to wrap a goroutine in a closure, and the closure takes care of the bookkeeping.

Hence we have not directly called the makeArray function a goroutine but created a goroutine closure and inside of that we have called the makeArray function.

We have created a channel named ch  that accepts an array of integers, then inside the closure, we call the makeArray function twice and then we are reading the value from the channel and saving it in the val1 and val2 variables.

Chat API Trusted by world’s biggest corporations | DeadSimpleChat
Chat API and SDk that supports 10 Million Concurrent Users. Features like Pre-Built turn key Chat Solution, Chat API’s, Customization, Moderation, Q&A, Language Translation.

Buffered Channels

By default the channels are unbuffered, meaning once a goroutine writes to a channel it will pause until another goroutine reads from the same channel.

Likewise, after a read from an unbuffered channel will pause until something is written to the channel.

A Buffered Channel accepts writes to a channel, without the corresponding read to the channel without blocking, and you need to specify the number of writes it accepts at the time of channel creation.

To create a buffered channel, specify the capacity of the buffer at the time of channel creation:

ch := make(chan int, 10)

In the above code snippet we have created a buffered channel that accepts 10 integer values without blocking.

After 10 integer values have to been written to the channel it will wait for a read from the channel before writing to the channel.

Here is a very simple Buffer example:

package main

import "fmt"

func main() {

	ch := make(chan string, 2);

	ch <- "hello"
	ch <- "world"

	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

If you make the channel unbuffered then the code will result in an error.

We will go over some more concepts, like closing the channel and iterating over channels and then show you an example that makes use of all of them.

Closing Channels

When you are done writing to a channel you can close the channel. Once the channel is a closed attempt to write to the channel would result in a panic.

To close the channel use the built-in close method and pass it the channel

close(ch)

Reading from a closed channel does not result in any error. If the channel is buffered and values have not been read from the channel then it will return the values. If all the values have been read then it will return zero value.

To check if the channel is opened or closed you can use the command ok idiom.

value, ok := <- ch

If the channel is open ok is set to true and if the channel is closed ok is set to false.

The responsibility of closing the channel is of the Goroutine writing to the channel. Closing the channel is only required if some other Gorouting is waiting for the channel to close, as we will see in the next example when iterating over the channel using the For range loops.

Chat API Trusted by world’s biggest corporations | DeadSimpleChat
Chat API and SDk that supports 10 Million Concurrent Users. Features like Pre-Built turn key Chat Solution, Chat API’s, Customization, Moderation, Q&A, Language Translation.

For range and Iterating over Channels

In our first channel example, we have launched two goroutines and then fetched the data from the two goroutines and saved it into the variables v1 and v2.

But what if we have launched tens or hundreds of goroutines then it gets tedious very soon to write variables v1, v2 .. v100.

To solve this problem we can iterate over the channel using the for range operator

Consider the example below:

package main

import (
	"fmt"
	"math/rand"
)

func makeArray(length int) []int {
	var array = []int{}

	for i := 0; i < length; i++ {
		array = append(array, rand.Intn(10))
	}

	return array;
}

func main() {

	ch := make(chan []int)
	for i := 0; i < 10; i++ {
		go func ()  {
			randomArray := makeArray(10)
			ch<- randomArray
		}()
	}

	for v := range ch {
		fmt.Println(v)
	}
}

In the above code, we are launching the Goroutines inside of a for loop, and we are launching 10 goroutines.

We are using the for range construct and iterate over our channel, but when we run this code it will print 10 arrays with random integers but exit with an error.

all goroutines are asleep - deadlock

This error occurs because we have not closed our channel after we have finished writing to it.

Our for-range loops wait for the data from the channel forever and thus this error occurs.

If we are using a for-range loop to iterate over the channels, we have to close the channel after we have finished writing to it.

But let's see what happens when we simply call the close method after our for-range loop

func main() {

	ch := make(chan []int)
	for i := 0; i < 10; i++ {
		go func ()  {
			randomArray := makeArray(10)
			ch<- randomArray
        
		}()
	}
    
     close(ch); 
    
	for v := range ch {
		fmt.Println(v)
	}
}

If we do this and run our program before the goroutine can execute our main thread exits and we get no output.

If we add time.Sleep(time.Second * 1) at the end of our main method to force our main thread to remain active we get this error

panic: send on closed channel

This error occurs because we have closed the channel on the main thread before goroutines finished writing to the channel.

To solve this problem, we need to have some mechanism to synchronise the main thread with the goroutines and determine when all the goroutines have finished execution and then close the channel.

This is done using WaitGroups in the sync package which we will learn in the next section.

Synchronizing Goroutines using WaitGroups and Defer

Go has a built-in package called as sync that allows synchronising goroutine execution and determining when the goroutines have finished execution.

To use the sync package, we create a WaitGroup and each time the goroutine is executed we call the Add() method on the WaitGroup.

Inside the goroutine, we call the Done() method when the goroutine finishes execution.

Then finally we have the Wait() method and the code after the Wait() method executes when all the goroutines have finished execution.

Let's update our example to use the WaitGroup

...
var wg sync.WaitGroup
func main() {
	ch := make(chan []int)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func ()  {
			defer wg.Done()
			randomArray := makeArray(10)
			ch<- randomArray
		}()
	}			
	wg.Wait()
	close(ch)

	for v := range ch {
		fmt.Println(v)
	}
}

In the above code snippet, we have created a variable named wg which is a WaitGroup

Each time our goroutine closure is called we are calling wg.Add(1) on the goroutine.

Inside the goroutine closure, you can see that we have added a defer statement and called wg.Done() at the start of the method.

defer is a special keyword in Golang. If you add the keyword defer before any code statement it executes after the method has been executed.

So this makes sure, even if the method results in a Panic, the statement preceding the defer keyword always executes.

But if you run this program it will result in an error:

fatal error: all goroutines are asleep - deadlock!

Why this error has occurred? As we have learned about unbuffered channels when something is written to the unbuffered channel the go-runtime waits until the channel is read.

And we are launching 10 goroutines that are writing to the same channel, and with the WaitGroup we are waiting for the goroutines to finish execution before we read from the channel using our for range loop.

To solve this problem, we just make our channel buffered and update the line to

ch := make(chan []int, 10);

Here is our complete code:

package main

import (
	"fmt"
	"math/rand"
	"sync"
)

func makeArray(length int) []int {
	var array = []int{}

	for i := 0; i < length; i++ {
		array = append(array, rand.Intn(10))
	}

	return array;
}

var wg sync.WaitGroup
func main() {
	ch := make(chan []int, 10);
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func ()  {
			defer wg.Done()
			randomArray := makeArray(10)
			ch<- randomArray
		}()
	}			
	wg.Wait()
	close(ch)

	for v := range ch {
		fmt.Println(v)
	}
}

Now our code successfully executes without any errors.  

Chat API Trusted by world’s biggest corporations | DeadSimpleChat
Chat API and SDk that supports 10 Million Concurrent Users. Features like Pre-Built turn key Chat Solution, Chat API’s, Customization, Moderation, Q&A, Language Translation.

Select Statement

The select statement allows us to concurrently wait on multiple channels without blocking.

As a very simple example, consider the code below:

package main

import (
	"fmt"
	"math/rand"
)

func makeArray(length int) []int {
	var array = []int{}

	for i := 0; i < length; i++ {
		array = append(array, rand.Intn(10))
	}

	return array;
}

func main() {
	c1 := make(chan []int)
	c2 := make(chan []int)
	
	go func ()  {
		randomArray := makeArray(10)
		c1<- randomArray
	}()

	go func ()  {
		randomArray := makeArray(10)
		c2<- randomArray
	}()

	
    select {
    case v1 := <-c1:
        fmt.Println(v1)
    case v2 := <-c2:
        fmt.Println(v2)
    }
}

We have created two channels c1 and c2 using the select we are waiting on both channels and as soon as the data arrives, we are exiting the code.

➜  go-examples go run myexample.go 
[1 7 7 9 1 8 5 0 6 0]

And the code output one array of random integers, if we want to wait for both channels when we can wrap it in a for select construct.

But when wrapping the code in a for select construct make sure to add a condition to exit the loop, otherwise, the loop runs forever and your code will result in a panic.

package main

import (
	"fmt"
	"math/rand"
)

func makeArray(length int) []int {
	var array = []int{}

	for i := 0; i < length; i++ {
		array = append(array, rand.Intn(10))
	}

	return array;
}

func main() {
	c1 := make(chan []int)
	c2 := make(chan []int)
	
	go func ()  {
		randomArray := makeArray(10)
		c1<- randomArray
	}()

	go func ()  {
		randomArray := makeArray(10)
		c2<- randomArray
	}()

	for {
		select {
		case v1 := <-c1:
			c1 = nil
			fmt.Println(v1)
		case v2 := <-c2:
			c2 = nil
			fmt.Println(v2)
		}
		if c1 == nil && c2 == nil {
			return
		}
	}

}

We are setting the channel to nil after we received value from the channel, then we check if both the channels are nil if yes then we are exiting the loop.

Conclusion

We conclude our blog post on Go channels, hopefully, by now, you would have a good idea on what Go Channels are, how they are used in a Goroutine, and how to to create a channel, close a channel and iterate over channels.

We have also discussed how to use synchronise the goroutines and how to use the select statement to listen to multiple channels in a non-blocking manner.

Add Chat to your App or Website with Dead Simple Chat

Dead Simple Chat

Dead Simple Chat offers powerful Chat API and SDK to add Live Streaming, Group and 1-1 Chat to your App, Website, Live Stream or Virtual Event.

The Chat can be customised to fit any use-case, from Sass application, Social Platform, and Education to Virtual Events.