LanguagesUnderstanding Channels in Go

Understanding Channels in Go

Go Programming ``

Goroutines made concurrent execution of multiple subprograms cheaper, which was a great benefit for Go developers. These peer routines need to communicate with each other and thankfully, Golang provides channels to help facilitate this interaction. Channels are similar to UNIX pipes, which help in communicating across concurrent goroutines. But, unlike pipes, channels are typed in such a way that encourages programming styles that are scalable with minimum debugging complexities. This Go programming tutorial will explore the basics of channels used with goroutines, complete with coding examples.

Before we begin, however, you may wish to read our article: Introduction to Goroutines in Go.

What Are Channels in Go Programming?

The design and principle of communication through channels is heavily influenced by Hoare’s CSP (Communicating Sequential Process) model, although it has been modified and evolved substantially since its proposal.

Channels behave like a UNIX pipe, where developers can put some data at one end and receive data back at the other end. They also support buffering with a configurable buffer size. Pipes, on the other hand, send data as a sequence of bytes and treat it generically. Programmers have to decide on the type of data and cast the information back to its acceptable form.

In the case of channels, Go developers specify the type of values to be passed. In case we want to pass values of any type, we can use the interface type; it will be the responsibility of the receiving end to detect the type and deal with it appropriately.

Read: Introduction to Go Programming

How Do You Create Channels in Go?

As stated, channels provide a communication bridge among several concurrently running goroutine activities. There can be more than one such channel. Each forms a passage for values of a specific type – called the element type – of the channel. For example, a channel that communicates int values is declared as chan int. We also can use a built-in make() function to create a channel in Go as shown in the following code example:

ch1 := make(chan int) //ch1 is an int type channel

The make() function creates a data structure of the channel and returns a reference. If we copy a channel or pass a channel as an argument to a function, we essentially are copying the reference. Like any other reference type, nil represents a zero value on the channel reference.

How to Send and Receive Through Channels in Go

The two primary operations associated with channels are send and receive. By sending, we transmit a value to another goroutine that is expecting a value and uses the receive operation to get it. The channel is used as a medium or conduit in the whole communication process. Go uses the <- operator for both send and receive operations. Here is an example of using send and receive in Go:

ch1 <- ival // this is a send statement
ival = <- ch1 //this is a receive statement

In the send statement, the operator <- separates the channel and the value operands. The operator is used to put some value in the channel (recall our UNIX pipe example). Meanwhile, in the receive expression, the operator is used to extract values from the channel and then assign them to a particular variable. If we want to discard the specific value returned from the channel, developers can just ignore the assignment as in the follow Go code example:

<- ch1 // received value ignored

How to Close a Channel in Go

There is a built-in close() function associated with Golang channels. The close() function essentially means that the communication for the channel is closed; no more values can be sent or received via this channel. To close a channel in Go, you use the code:

close(ch1)

Read: How to Use Strings in Go

How to Create Buffered and Unbuffered Channels in Go

We can create both buffered and unbuffered channels using the built-in make() function. Go developers can directly provide the size of the buffer as an argument to the make() function. For example, programmers can simply declare the channel using the code:

ch1 = make(chan int) // unbuffered channel

The above Go code example would create an unbuffered channel. We also can create an unbuffered channel by providing the value 0 as the second argument:

ch1 = make(chan int, 0) //also creates unbuffered channel

However, if we provide a non-zero value as a second argument, a buffer would be created with the initial capacity of the value provided as the second argument. Here is how that looks in code:

ch1 = make(chan int, 5) // buffered channel, capacity=5

Communicating Via the Unbuffered Channel

Here is a quick code example showing how to send and receive messages between two active goroutines in Go:

func main() {
	ch1 := make(chan string)
	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		msg := "hello"
		defer wg.Done()
		fmt.Println("Message sent:" + msg)
		ch1 <- msg
	}()

	go func() {
		defer wg.Done()
		time.Sleep(time.Second * 1)
		rmsg := <-ch1
		println("Message received:" + rmsg)
	}()

	wg.Wait()
}

In the above code example, we have created a channel with an empty list of receivers and senders. Note that we have created an unbuffered channel in this case (because the second argument of the make() function is empty). Also, note that we have created a WaitGroup. The WaitGroup is used to cause the application to wait for all the goroutines launched to finish their execution. With each new goroutine, we increment the counter; in the above example, we have only two goroutines, so the counter value is provided with the statement wg.Add(2).

The first goroutine – or the sender – sends the message, which is then enqueued and put into a waiting state. The second goroutine reads via the channel, which dequeues the message. The channel internally uses the memmove() function to copy values from the sender to the variable (rmsg) that reads the channel.

Read: Understanding Mathematical Operators in Go

Communicating via Buffered Channels in Go

Now, let’s slightly modify the above example and add a buffer to the channel:

func main() {
	ch1 := make(chan string, 2)
	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		msg1 := "hi"
		msg2 := "bye"
		defer wg.Done()
		fmt.Println("Message sent: " + msg1 + " and " + msg2)
		ch1 <- msg1
		ch1 <- msg2
	}()

	go func() {
		defer wg.Done()
		time.Sleep(time.Second * 1)
		rmsg1 := <-ch1
		rmsg2 := <-ch1
		println("Message received: " + rmsg1 + " and " + rmsg2)
	}()

	wg.Wait()
}

Observe that the buffered channel has an initial capacity of 2. Internally, the buffer is maintained in a circular queue. Once the buffer is full, the goroutine which tries to push an element into the buffer to the sender list switches to a waiting state. The waiting goroutine resumes pushing the value as soon as the buffer is emptied by receiving its value through the dequeued goroutine.

Final Thoughts on Channels in Golang

This was a quick overview of Go channels and how Golang programmers can use them with goroutines. Understand that the choice between buffered and unbuffered channels and their initial capacity directly affects the smooth execution of the multiple goroutines. Unbuffered channels are somewhat better because send and receive operations are synchronized, while, with buffered channels, they are decoupled. If developers fail to allocate sufficient buffer capacity, the program can be in a deadlock state. As mentioned, channel buffering always affects program performance.

Read more Go and Golang programming tutorials and how-tos.

Latest Posts

Related Stories