Apps/Gaming

Understand channels in Go

Goroutines made it cheaper to run multiple subroutines at the same time, which was a huge benefit for Go developers. These peer routines need to communicate with each other, and luckily Golang provides channels to facilitate this interaction. Channels are similar to UNIX pipes that help communicate between concurrent goroutines. However, unlike pipes, channels are typed to encourage programming styles that are scalable with minimal debugging complexities. This Go programming tutorial explores the basics of channels used with goroutines, including coding examples.

What are channels in Go programming?

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

Channels behave like a UNIX pipe, where developers can put some data on one end and get data back on 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 them generically. Programmers must decide on the nature of the data and bring the information back into its acceptable form.

In the case of channels, Go developers specify the type of values ​​to be passed. If we want to pass values ​​of any type, we can use the interface type; It is the receiving party’s responsibility to recognize the type and deal with it appropriately.

Read: Introduction to Go programming

How to create channels in Go?

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

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

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

How to send and receive channels in Go

The two main operations associated with channels are sending and receiving. When sending, we transfer a value to another goroutine that expects a value and uses the receive operation to get it. The channel is used as a medium or channel in the whole communication process. Go uses the <- operator for both send and receive operations. Here's an example of using Send and Receive in Go:

ch1 <- ival // This is a send instruction. ival = <- ch1 // This is a receive statement

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

<- ch1 //received value ignored

How to close a channel in Go

Golang channels have a built-in close() function associated with them. The close() function essentially means that the communication for the channel is closed; Values ​​can no longer be sent or received via this channel. To close a channel in Go use the code:

close (ch1)

Read: How to use strings in Go

How to create buffered and unbuffered channels in Go

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

ch1 = make(chan int) // unbuffered channel

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

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

However, if we supply a non-zero value as the second argument, a buffer will be created with the initial capacity of the value supplied as the second argument. This is what it looks like in code:

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

Communication via the unbuffered channel

Here is a short 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("Received message:" + rmsg) }() wg.Wait() }

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

The first goroutine—or the sender—sends the message, which is then queued and placed in a wait state. The second goroutine reads through the channel, which dequeues the message. The channel internally uses the memmove() function to copy values ​​from the sender into the variable (rmsg) that reads the channel.

Read: Understand mathematical operators in Go

Communication via buffered channels in Go

Now let’s slightly modify the example above 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("Received message: " + rmsg1 + " and " + rmsg2) }() wg.Wait() }

Note that the buffered channel has an initial capacity of 2. Internally, the buffer is held in a circular queue. Once the buffer is full, the goroutine trying to push an item into the buffer to the senders list enters a wait state. The waiting goroutine continues to push the value once the buffer is emptied by receiving its value through the dequeued goroutine.

Final Thoughts on Channels in Golang

This was a brief 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 slightly better because transmit and receive operations are synchronized while buffered channels are decoupled. If developers don’t allocate enough buffer capacity, the program can end up in a deadlock state. As mentioned earlier, channel buffering always affects program performance.

Continue reading Go and golang programming tutorials and guides.

Related posts

Best DevOps and DevSecOps Tools

TechLifely

Hybrid casual: the secret sauce to higher retention and better engagement

TechLifely

Best Practices for Multithreading in Java

TechLifely

Leave a Comment