Advanced GoLang Concepts: Channels, Context, and Interfaces.

Rebeccah Wambui
4 min readSep 13, 2023

--

Photo by Denys Nevozhai on Unsplash

Introduction

In this article, we will cover some advanced GoLang concepts in detail. In Go, these are some of the building blocks. They are tools that you can use to make your code clean, scalable, and maintainable especially when your project grows in complexity.

It’ll be pretty cool if you open your IDE, and follow the steps, as we learn together! Let’s get into it!

Channels

Channels are fundamental tools for achieving concurrency in Go. They are used to communicate and transfer data between go-routines. The communication is bidirectional by default, which means you can send and receive messages from the same go-routine

Implementation 👩‍💻

package main

import "fmt"

func main() {
dataChan := make(chan string) //adds data to the channel
dataChan <- "Hey Champ!" // gets data from the channel

c := <- dataChan

fmt.Println(c)
}

In this function— ;

- We’re declaring our channel, dataChan and explicitly stating the data type (String)

- Send data to the channel

- Receiving the data from the channel

- Print the data using Println

If we run this function, we get the following results;

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
/Users/rebeccahwambui/Beck/golang-concepts/channels/main.go:7 +0x3c
exit status

The reason why we’re getting a deadlock error is because, by default, if you put data into a channel, you also need to create an exit, this process needs to be done simultaneously.

Both of these lines;

dataChan := make(chan string)

dataChan <- “Hey Champ!"

need to execute simultaneously, we can achieve that by;

1. Creating another go-routine;

package main

import "fmt"

func main() {
dataChan := make(chan string) //adds data to the channel

go func() { // go-routine that's running on the background thread
dataChan <- "Hey Champ!" // gets data from the channel
}()

c := <- dataChan

fmt.Println(c)
}

If we run this now, we get results;

go run .
Hey Champ!

We now have the channels running on different go-routines and can be executed simultaneously!

2. Creating a Buffered Channel;

When we ran our first block of code, we got a deadlock error because our channel didn’t have any space init. Technically, you can create some space by making the channel a Buffered channel.

What is a buffered Channel?

Buffered channels in Golang are used to communicate concurrently executing functions by sending and receiving information from a certain element type for a given capacity.

They work best when you want to get back data from a set(s) of go-routines or if you want to limit concurrent usage. They are also helpful for managing the amount of work a system has queued up, preventing your program from becoming overwhelmed.

Implementation 👩‍💻

func main() {
dataChan := make(chan string, 1) // buffered channel

dataChan <- "Hey Champ!" //add data to the channel
c := <-dataChan // get data from the channel

fmt.Println(c)
}

Response;

go run .
Hey Champ!

Note: This is only used to set limited number of data, if we try to add data to the channel without updating the space, you will get a deadlock error, because we’ve explicitly sayed we’re adding only one value to the channel,so the channel only expects a single value.

func main() {
dataChan := make(chan string, 1)

dataChan <- "Hey Champ!"
dataChan <- "GoLang is awesome!"
c := <-dataChan

fmt.Println(c)
}
go run .
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()

exit status 2

We’ve gone through the high level of what channels are and how they work. Technically, channels are used in a multi-threading context; used with different go-routines.

We’ll take a dip-dive at what this looks like in a simple function:

From this block of code;

package main

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

func DoWork() int {
time.Sleep(time.Second)
return rand.Intn(10)
}

func main() {
dataChan := make(chan int)

go func() { // go-routine that's running on the background thread
wg := sync.WaitGroup{}

for i := 0; i < 10; i++ {
wg.Add(1)

go func() { // go-routine that's running on the background thread
defer wg.Done()
result := DoWork()
dataChan <- result
}()
}

wg.Wait()
close(dataChan)
}()

for x := range dataChan {
fmt.Printf("%d\n", x)

}

}
$ We create a func DoWork() which returns an int

func DoWork() int {
time.Sleep(time.Second) // sleep for 1 sec
return rand.Intn(10) // return a random int from 1 - 100
}

$ In our main fun, we have our unbuffered chan which takes in an int
dataChan := make(chan int)

$ In our first go-routine,we create a for loop, which for every iteration,
of the function, it creates a new go-routine that does the work,
and sends the results to the dataChan

for i := 0; i < 10; i++ {
wg.Add(1)

go func() { // go-routine that's running on the background thread
defer wg.Done()
result := DoWork()
dataChan <- result // send results to the dataChan
}()

}

$ To prevent the deadlock error, we need to track and wait for
the go-routines to complete the loop. For that,we use the WaitGroup.
For each iteration we add 1 to the WaitGroup

for i := 0; i < 10; i++ {
wg.Add(1)

- In the iteration, we defer wg.Done()
defer wg.Done()

- In the end, we wait until all the iterations are done, before we close
the channel

wg.Wait()
close(dataChan)

$ Lastly we print out the results

for x := range dataChan {
fmt.Printf("%d\n", x)

}

Results:

go run .
1
7
7
9
1
8
5
0
6
0

I hope this is helpful 🙂 Let me know in the comments section. Feel free to share your feedback and questions as well.

Happy learning 🤓

You can find Part 2 on Context and Interfaces here.

--

--

Rebeccah Wambui

Software Engineer @Twigafoods || Problem Solver || Happy to share and learn with and from You all 🌟