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.
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 }
In this section, we’ll review Go channels’ uses and how they can benefit app development.
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.
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.
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") }
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:
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.
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.
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.
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
.
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)
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
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! 🙂
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.