Salah Elhossiny Currently working as a FullStack ML developer. AWS Community Builder, certified AWS ML specialist, and Technical Content Writer at freeCodeCamp and Decoded For Devs.

How to use Go channels

6 min read 1875

How To Use Go Channels

A Go channel is a communication mechanism that allows Goroutines to exchange data. When developers have numerous Goroutines running at the same time, channels are the most convenient way to communicate with each other.

Developers often use these channels for notifications and managing concurrency in applications.

In this post, we’ll cover the general uses of Go channels, including how to write into and read from a channel, how to use channels as function parameters, and how to use range to iterate over them.

Creating a Go channel structure

To begin, let’s create a channel in Go using the make function:

// for example if channel created using following : 
ch := make(chan string)

// this is the basic structure of channels 
type hchan struct {
  qcount uint   // total data in the queue
  dataqsiz uint  // size of the circular queue 
  buf  unsafe.Pointer // pointer to an array of dataqsiz elements
  elementSize uint16 
  closed uint32 
  sendx  uint // send index 
  recvx  uint // receive index 
  recvq waitq // list of receive queue 
  sendq  waitq // list of send queue 
  lock mutex // lock protects all fields in hchan, as well as several
}

Go channels uses

In this section, we’ll review Go channels’ uses and how they can benefit app development.

Using Go channels as futures and promises

Developers often use futures and promises in Go for requests and responses. For example, if we want to implement an async/await pattern, we must add the following:

package main 

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

func longTimedOperation() <-chan int32 {
  ch := make(chan int32)
  func run(){
    defer close(ch)
    time.Sleep(time.Second * 5)
    ch <- rand.Int31n(300)
  }
  go run()
  return ch
}

func main(){
  ch := longTimedOperation()
  fmt.Println(ch)
}

By simply simulating a long-running process using a 5-second delay, we can send a random integer value to a channel, wait for the value, and receive it.

Using Go channels for notifications

Notifications are one-of-a-kind requests or responses that return values. We usually use a blank struct type as the notification channel element type because the size of the blank struct type is zero, meaning the values of the struct don’t consume memory.

For example, implementing a one-to-one notification with a channel receives a notification value:

package main 

import (
  "fmt"
  "time"
) 
type T = struct{}

func main() {
  completed := make(chan T)
  go func() {
    fmt.Println("ping")
    time.Sleep(time.Second * 5) // heavy process simulation
    <- completed // receive a value from completed channel
  }

  completed <- struct{}{} // blocked waiting for a notification 
  fmt.Println("pong")
}

This lets us use a value received from a channel to alert another Goroutine waiting to submit a value to the same channel.

We made a custom demo for .
No really. Click here to check it out.

Channels can also schedule notifications:

package main

import (
  "fmt"
  "time"
) 

func scheduledNotification(t time.Duration) <- chan struct{} {
  ch := make(chan struct{}, 1) 
  go func() {
    time.Sleep(t)
    ch <- struct{}{}
  }()
  return ch
}

func main() {
    fmt.Println("send first")
    <- scheduledNotification(time.Second)
    fmt.Println("secondly send")
    <- scheduledNotification(time.Second)
    fmt.Println("lastly send")
}

Using Go channels as counting semaphores

To impose a maximum number of concurrent requests, developers frequently use counting semaphores to lock and unlock concurrent processes to control resources and apply mutual exclusions. For example, developers can control the read and write operations in a database.

There are two ways to obtain a piece of a channel semaphore ownership, similar to using channels as mutexes:

  1. Acquiring ownership with a send and releasing through a receive
  2. Taking possession with a receive and releasing with a send

However, there are some specific rules when owning a channel semaphore. First, each channel allows the exchange of a particular data type, which is also called the element type of the channel.

Second, for a channel to operate properly, someone must receive what is sent via the channel.

For example, we can declare a new channel using the chan keyword, and we can close a channel using the close() function. So, if we block the code using the < - channel syntax to read from the channel, once completed, we can close it.

Finally, when using a channel as a function parameter, we can specify its direction, meaning specifying whether the channel will be used for sending or receiving.

If we know the purpose of a channel in advance, use this capability because it makes programs more robust and safer. This means we can’t send data accidentally to a channel that only receives data, or receive data from a channel that only sends data.

As a result, if we declare that a channel function parameter will be used for reading only and we try to write to it, we get an error message that will most likely save us from nasty bugs.

Writing to a Go channel

The code in this subsection teaches us how to write to a channel in Go. Writing the value x to channel c is as easy as writing c <- x.

The arrow shows the direction of the value; we’ll have no problem with this statement as long as both x and c have the same type.

In the following code, the chan keyword declares that the c function parameter is a channel and must be followed by the type of the channel, which is int. Then, the c <- x statement allows us to write the value x to channel c, and the close() function closes the channel:

package main
import (
  "fmt"
  "time"
)

func writeToChannel(c chan int, x int) {
   fmt.Println(x)
   c <- x
   close(c)
   fmt.Println(x)
}

func main() {
    c := make(chan int)
    go writeToChannel(c, 10)
    time.Sleep(1 * time.Second)
}

Finally, executing the previous code creates the following output:

$ go run writeCh.go 
10

The strange thing here is that the writeToChannel() function prints the given value only once, which is caused when the second fmt.Println(x) statement never executes.

The reason for this is pretty simple: the c <- x statement blocks the execution of the rest of the writeToChannel() function because nobody is reading what was written to the c channel.

Therefore, when the time.Sleep(1 * time.Second) statement finishes, the program terminates without waiting for writeToChannel().

The next section illustrates how to read data from a channel.

Reading from a Go channel

We can read a single value from a channel named c by executing <-c. In this case, the direction is from the channel to the outer scope:

package main
import (
"fmt"
"time"
)

func writeToChannel(c chan int, x int) {
  fmt.Println("1", x)
  c <- x
  close(c)
  fmt.Println("2", x)
}

func main() {
  c := make(chan int)
  go writeToChannel(c, 10)
  time.Sleep(1 * time.Second)
  fmt.Println("Read:", <-c)
  time.Sleep(1 * time.Second)
  _, ok := <-c

  if ok {
    fmt.Println("Channel is open!")
  } else {
    fmt.Println("Channel is closed!")
  }
}

The implementation of the writeToChannel() function is the same as before. In the preceding code, we read from channel c using the <-c notation.

The second time.Sleep(1 * time.Second) statement gives us the time to read from the channel.

The current Go code works fine when the channel is closed; however, if the channel was open, the Go code presented here would have discarded the read value of the channel because we used the _ character in the _, ok := <-c statement.

Use a proper variable name instead of _ if we also want to store the value found in the channel in case it is open.

Executing readCh.go generates the following output:

$ go run readCh.go
1 10
Read: 10
2 10
Channel is closed!
$ go run readCh.go
1 10
2 10
Read: 10
Channel is closed!

Although the output is still not deterministic, both the fmt.Println(x) statements of the writeToChannel() function execute because the channel unblocks when we read from it.

Receiving from a closed channel

In this subsection, we’ll review what happens when we try to read from a closed channel using the Go code found in readClose.go.

In this part of the readClose.go program, we must create a new int channel named willClose to write data to it, read the data, and close the channel after receiving the data:

package main
import (
  "fmt"
)

func main() {
  willClose := make(chan int, 10)
  willClose <- -1
  willClose <- 0
  willClose <- 2
  <-willClose
  <-willClose
  <-willClose
  close(willClose)
  read := <-willClose
  fmt.Println(read)
}

Executing the previous code (saved in readClose.go file) generates the following output:

$ go run readClose.go
0

This means that reading from a closed channel returns the zero value of its data type, which in this case is 0.

Channels as function parameters

While we did not use function parameters when working with readCh.go or writeCh.go, Go does allow us to specify the direction of a channel when using it as a function parameter, meaning whether it’s used for reading or writing.

These two types of channels are called unidirectional channels, whereas channels are bidirectional by default.

Examine the Go code of the following two functions:

func f1(c chan int, x int) {
 fmt.Println(x)
 c <- x
}

func f2(c chan<- int, x int) {
 fmt.Println(x)
 c <- x
}

Although both functions implement the same functionality, their definitions are slightly different. The difference is created by the <- symbol found on the right of the chan keyword in the definition of the f2() function.

This denotes that the c channel can only write. If the code of a Go function attempts to read from a write-only channel (also known as a send-only channel) parameter, the Go compiler generates the following error message:

# command-line-arguments
a.go:19:11: invalid operation: range in (receive from send-only type chan<-int)

Similarly, we can have the following function definitions:

func f1(out chan<- int64, in <-chan int64) {
  fmt.Println(x)
  c <- x
}

func f2(out chan int64, in chan int64) {
  fmt.Println(x)
  c <- x
}

The definition of f2() combines a read-only channel named in with a write-only channel named out. If we accidentally try to write and close a read-only channel (also known as a receive-only channel) parameter of a function, we get the following error message:

# command-line-arguments
a.go:13:7: invalid operation: out <- i (send to receive-only type <-chan int)
a.go:15:7: invalid operation: close(out) (cannot close receive-only 
channel)

Range over Go channels

We can use range syntax in Golang to iterate over a channel to read its values. Iterating here applies the first-in, first-out (FIFO) concept: as long as we add data to the channel buffer, we can read from the buffer like a queue:

package main

import "fmt"

func main() {

    ch := make(chan string, 2)
    ch <- "one"
    ch <- "two"
    close(ch)

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

As mentioned above, using range to iterate from a channel applies the FIFO principle (reading from a queue). So, executing the previous code outputs the following:

$ go run range-over-channels.go
one
two

Conclusion

Go channels are used for communicating between concurrently running functions by sending and receiving a specific element type’s data. When we have numerous Goroutines running at the same time, channels are the most convenient way for them to communicate with one another.

Thanks for reading and happy coding! 🙂

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Salah Elhossiny Currently working as a FullStack ML developer. AWS Community Builder, certified AWS ML specialist, and Technical Content Writer at freeCodeCamp and Decoded For Devs.

Leave a Reply