Go, also known as Golang, is a powerful programming language that is known for its ability to handle concurrency efficiently. And the feature which enables this is channels. In this blog post, we will explore the power of channels in Golang by looking at some example use cases. We will cover and some advanced techniques for using them in your Go programs. By the end of this article, I hope you’ll have a good understanding of how to use channels in your program and improve performance and scalability of your go application.
Agenda
Despite channels being a widely use feature in Golang, many struggle to use them in their Golang programs. Therefore, in this tutorial, we will go through a few examples of how to use channels in your programs. Also I am sure these examples aren’t the only ways we can make use of channels. But it will hopefully help you gain better understanding of how to use them by the end of the tutorial. Let’s begin.
Receive Http response on a channel
Let’s first define a Response “object” AjaxResponse. Below will be the format of our http response we’ll be receiving on our channel which you will see shortly.
type AjaxResponse struct {
data []byte
statusCode int
err error
}
To make an HTTP request, we’ll create a function that takes a URL and makes a GET request. We will call this function “ajax“, but you can name it whatever you like. We will not go into detail about this function, as it is a standard GET request/response. The only important part of the code that requires your attention is as follows:
- line 2 – we have created a respChan channel of type AjaxResponse.
respChan := make(chan AjaxResponse) - line 37 – we have returned our respChan from our ajax function.
return respChan - line 8, 22, 30 – we send in a AjaxResponse on our respChan based on the success/failure of our request.
func ajax(url string) <-chan AjaxResponse {
respChan := make(chan AjaxResponse)
go func() {
// make a request
resp, err := http.Get(url)
if err != nil {
respChan <- AjaxResponse{
nil,
resp.StatusCode,
err,
}
return
}
defer resp.Body.Close()
// read all body atonce
b, err := io.ReadAll(resp.Body)
if err != nil {
respChan <- AjaxResponse{
nil,
http.StatusInternalServerError,
err,
}
return
}
respChan <- AjaxResponse{
b,
resp.StatusCode,
nil,
}
}()
return respChan
}
Please note that line 7 (reading from a channel) will block until we receive a response from our ajax function.
func main() {
respChan := ajax("https://strapengine.com/")
// as respChan is a channel we need to read from it.
resp := <-respChan
if resp.err != nil {
fmt.Println(resp.statusCode)
} else {
fmt.Println(resp.statusCode)
}
}
Using the above ajax function we just created, we can also perform a “kindOfPromiseAll” as demonstrated below. It accepts an array of AjaxResponse channels and returns an array of byte array response data.
- line 5 – we create channel of type AjaxResponse
- line 8 -12 – for each channel, we create a new goroutine, which reads from the channel and sends the data to the resCh channel. This way we can read from all the channels in the list concurrently.
- line 15 – 22 – here we start reading from our resCh one result at a time and append the results to our final results array.
func kindOfPromiseAll(items []<-chan AjaxResponse) ([][]byte, error) {
var result [][]byte
// we create a result channel of type AjaxResponse
resCh := make(chan AjaxResponse)
// we loop through our items array
for _, ch := range items {
go func(ch <-chan AjaxResponse) {
resCh <- <-ch
}(ch)
}
// we read result from our resCh
for range items {
res := <-resCh
if res.err != nil {
close(resCh)
return nil, res.err
}
result = append(result, res.data)
}
return result, nil
}
func main() {
result, err := kindOfPromiseAll(
[]<-chan AjaxResponse{
ajax("https://strapengine.com/go-fiber-custom-header-middleware/"),
ajax("https://strapengine.com/docker-swarm-tutorial/"),
},
)
if err != nil {
fmt.Println(err)
}
fmt.Println(result)
}
Channels in place of wait group
We can use channels in Go as an alternative to wait groups for synchronizing goroutines.
- line 3 – we create a done channel(buffered) for signaling
- line 6 – 15 – we create an individual go routine for each of our tasks. On line 13, we are indicating the completion of a task by a goroutine by sending a signal on the buffered “done” channel. This allows us to send signals without the need to start receiving them immediately.
- line 18 – 20 – we also need to drain our done channel at the end. This will ensure that until each goroutines are done, program doesn’t exit.
func waitForGoroutines(numGoroutines int) {
// Create a channel with a buffer size of numGoroutines
done := make(chan bool, numGoroutines)
// Launch goroutines
for i := 0; i < numGoroutines; i++ {
go func() {
// Perform some work
fmt.Println("Doing work...")
time.Sleep(time.Second)
// Signal that the goroutine is done
done <- true
}()
}
// Wait for all goroutines to finish
for i := 0; i < numGoroutines; i++ {
<-done
}
fmt.Println("All goroutines finished.")
}
Invoke function periodically with ticker
Below we have a function runEveryInterval which takes in another function and executes it periodically at a given regular intervals. Here we have used channels for signaling termination.
- line 3 – we have created a new ticker
- line 6 – we create a done channel
- line 8 – 19 – the returned ticker(line 3) is of type
*Ticker
, which is a struct that contains a channelC
on which the current time will be sent at the specified interval. This is the heart of our program.
As you can see we also have our done channel in our select statement so that we can return/exit from our function when we receive a value. - line 22 – 25 – we return a function which is responsible for stopping our ticker and signaling termination on our done channel.
func runEveryInterval(fn func(), duration time.Duration) func() {
// Create a new ticker
ticker := time.NewTicker(duration)
// Create a channel
done := make(chan bool)
go func(fn func()) {
for {
select {
case <-done:
return
// ticker.C is a channel
case <-ticker.C:
fn()
}
}
}(fn)
// return a function
return func() {
ticker.Stop()
done <- true
}
}
Let’s test our runEveryInterval function
func main() {
stop := runEveryInterval(func() {
// the function code we want to execute
fmt.Println("Tick")
}, 2*time.Second)
// we sleep for 10 seconds
time.Sleep(10 * time.Second)
// calling stop would stop our ticker and signal done.
stop()
}
Make http requests with a timeout
The function runAjaxWithTimeout
utilizes a select
statement to determine which of two possible cases on a channel will be executed first. The first case is a channel returned by the ajax
function, and the second case is a channel returned by the time.After
function. Whichever channel returns first, the corresponding case in the select
statement will be executed.
func runAjaxWithTimeout(url string, duration time.Duration) ([]byte, error) {
select {
case resp := <-ajax(url):
if resp.err != nil {
return nil, resp.err
}
return resp.data, nil
case <-time.After(duration):
return nil, errors.New("timeout")
}
}
Fastest response first
Below we have another example function which takes in an array of channel of type AjaxResponse and returns the response from the channel from which we can read the fastest.
In other words, it’s creating a goroutine for each item in items, and the goroutine receives data from the channel and send it to another channel(resCh). And at the end using a select we try to read the very first value from our resCh and return it.
func kindOfPromiseRace(items []<-chan AjaxResponse) ([]byte, error) {
resCh := make(chan AjaxResponse)
defer close(resCh)
for _, ch := range items {
go func(ch <-chan AjaxResponse) {
resCh <- <-ch
}(ch)
}
select {
case resp := <-resCh:
if resp.err != nil {
return nil, resp.err
}
return resp.data, nil
}
}
A possible use case of the above code is in situations where we may want to query data from multiple sources and resolve with the fastest response in order to minimize response time.
func main() {
result, err := kindOfPromiseRace(
[]<-chan AjaxResponse{
ajax("https://strapengine.com/go-fiber-custom-header-middleware/"),
ajax("https://strapengine.com/docker-swarm-tutorial/"),
},
)
if err != nil {
fmt.Println(err)
}
fmt.Println(result)
}
Channels as lock
We can also use channels in golang to synchronize access to shared variables. Taking benefit of this property, instead of using a traditional mutex lock, we can use a channel to ensure that only one goroutine is accessing the shared variable at a time.
We can achieve by sending a message on the channel before accessing the shared variable and receiving a message on the channel after finishing the access. This way, other goroutines will be blocked from accessing the shared variable until the current goroutine is done with it.
func mutexLock() {
// for mutex lock
mutex := make(chan struct{}, 1)
// for signaling
done := make(chan struct{})
// counter is our shared variable/resource
counter := 0
maxCount := 1000
incr := func(mutex chan<- struct{}, done chan<- struct{}) {
// writing to channel to obtain the lock
mutex <- struct{}{}
// code we want to be accessed exclusively(critical section)
counter++
done <- struct{}{}
}
for i := 0; i < maxCount; i++ {
go incr(mutex, done)
}
for range done {
// reading from channel to release the lock
<-mutex
if counter >= maxCount {
close(done)
}
}
fmt.Println("Counter", counter)
}
In the above example, we used two channels, one for synchronization (acting as a mutex) and the other for signaling. But we can also get almost the same result using a single channel for mutex and eliminating our done channel as show below.
func main() {
mutex := make(chan struct{})
// counter is our shared variable/resource
counter := 0
maxCount := 1000
incr := func(mutex chan struct{}) {
counter++
mutex <- struct{}{}
}
for i := 0; i < 1000; i++ {
go increment(mutex)
}
for i := 0; i < maxCount; i++ {
// reading from channel to release the lock
<-mutex
}
fmt.Println("Counter", counter)
}
Please do note it may not be efficient to use channels as mutexes. The example is just for the sake that we do so should we ever wanted to.
Broardcast pattern using channels
Channels in Go can be used to implement a broadcast system where a single message is sent to multiple recipients. Here, the sender sends the message on an input channel, and the message is broadcasted to multiple output channels. The recipients can then receive the messages from the output channels. This ensures that all recipients receive the same message, and the sender only needs to send the message once. By using channels, we can implement this broadcast mechanism in a concurrency-safe manner, which means that we can avoid race conditions and ensure that the we deliver messages safe.
func broadcast(outChs []chan string) chan<- string {
inCh := make(chan string)
go func() {
// Copy each message on input channel to all the output channels
for message := range inChannel {
for _, outCh := range outChs {
outCh <- message
}
}
// Closing all the output channels, once we are done with broadcasting
for _, outCh := range outChs {
close(outCh)
}
}()
return inCh
}
Let us have a look on how to use the above broadcast function.
func main() {
// Create an array of two string channels
outChs := []chan string{make(chan string), make(chan string)}
// Call the broadcast function with outChs as the argument and store the returned input channel in inChs
inCh := broadcast(outChs)
messages := []string{"message 1", "message 2", "message 3"}
// Feeding in our inCh is handled in a separate goroutine to prevent deadlock
go func() {
defer close(inCh)
// Feed in each message to our inCh
for _, message := range messages {
inCh <- message
}
}()
// Create a WaitGroup to wait for all the goroutines to complete
var wg sync.WaitGroup
for i := 0; i < len(outChs); i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
// Receive messages from the output channels, until it is closed
for message := range outChs[i] {
fmt.Println(message, "message received")
}
}(i)
}
// Wait for all the goroutines to complete
wg.Wait()
}
Fan In pattern using channels
The fan-in pattern in Go is a technique for merging multiple channels into a single output channel. The main goal of the fan-in pattern is to collect the results of many concurrent processes into a single channel, allowing a single consumer to process all of the results in a unified manner. We can implement the fan-in pattern using channels in Golang like below.
func fanIn(inChs []<-chan string) <-chan string {
outCh := make(chan string)
go func() {
defer close(outCh)
for _, inCh := range inChs {
for msg := range inCh {
outCh <- msg
}
}
}()
return outCh
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
ch3 := make(chan string)
inChs := []<-chan string{ch1, ch2, ch3}
// create our concurrent processes
process_1 := func() {
ch1 <- "Message 1 from channel 1"
close(ch1)
}
process_2 := func() {
ch2 <- "Message 1 from channel 2"
close(ch2)
}
process_3 := func() {
ch3 <- "Message 1 from channel 3"
close(ch3)
}
// calling our concurrent tasks
go process_1()
go process_2()
go process_3()
aggregateCh := fanIn(inChs)
// receive messages from the aggregateCh output channel
for msg := range aggregateCh {
fmt.Println(msg)
}
}
Fan out pattern using channel
A fan-out function in Golang is a pattern that allows us to distribute tasks or data among multiple workers. We can implement the fan-out pattern using channels in Golang by creating a function that takes an array of output channels and returns an input channel. The input channel acts as a multiplexer, allowing us to send data to it and have it distributed to all the output channels.
func fanOut(outChs []chan<- string) chan<- string {
inCh := make(chan string)
// we loop though each output channel to distribute task received on inCh
for _, outCh := range outChs {
go func(outCh chan<- string) {
for msg := range inCh {
outCh <- msg
}
}(outCh)
}
return inCh
}
Please do note that the above function doesn’t guarantee equal division of task among all the output channels. So let have a look at a simple example.
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
ch3 := make(chan string)
outChs := []chan<- string{ch1, ch2, ch3}
inCh := fanOut(outChs)
// send messages/task to the input channel
go func() {
for i := 0; i < 3; i++ {
inCh <- fmt.Sprintf("Message %d", i)
}
close(inCh)
}()
// receive messages from the output channels
for i := 0; i < 3; i++ {
// we cannot say anything about the order regarding which channel will receive a value first. So to prevent blocking we use a select so that we can read from the channels as they arrive.
select {
case msg1 := <-ch1:
fmt.Println("Message from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Message from ch2:", msg2)
case msg3 := <-ch3:
fmt.Println("Message from ch3:", msg3)
}
}
}
Conclusion
In conclusion, channels in Golang provide a way for safe and efficient communication between Goroutines. They are a powerful tool for concurrent programming that allows Goroutines to share data and coordinate their activities. There are a lot more ways that we can make use of channels, making them a versatile and valuable tool for writing concurrent applications in Golang.
Attribution
Gopher thumbnail image backgound source
One thought on “Channels in golang: Usage with examples”