Skip to main content

Channels in Golang

Channels In Golang

What are Channels?

Channels are conduits that connect two or more goroutines, It provides the mechanism for concurrently executing goroutines to communicate by sending or receiving values. Channel is by default bidirectional, i.e, a go routine can send and receive data from the same channel. But one can define a unidirectional channel as well. When declaring a channel, what data type will be transferred needs to be specified.

Channels are best suited when we want to:

  • Pass the ownership of the data to another goroutine
  • Distribute the units of work across multiple goroutines
  • Communicate data from child to parent goroutines

Goroutines passing data using channel

How to create channels?

To create a channel we use the keyword chan followed by the type of data which will be passed in the channel. Here's an example syntax for creating a channel

var ch chan int

Here we have declared a channel of type int. Since we haven't initialized the channel yet, hence, ch is a zero value channel or a nil channel the value of ch is nil. We can initialize the channel using the make() function. Here's an example of initializing a channel.

var ch chan int

ch = make(chan int)

Here we have declared a channel ch, which is initialized using the built-in make() function. We can combine the declaration and initialization of the channel using the short declaration operator

ch := make(chan int)

Operations on channel

We can perform the Read, Write and Close operation in a channel. We use the <- operator to read or write into the channel. Here's the syntax for reading and writing into a channel

channel <- value

value <- channel

Here's an example showing the read and write operation in a channel. You can run the program to check the output:

ouptut

In the above example, We have created two functions named write and read, both taking a channel of type string as a parameter. write function is putting a string message in the channel. read function will wait for a message in the channel. Once, It receives the message it will print the message and return it. In main function, We have created a channel named msgStream which is of type string. This channel will be used to pass the message from one goroutine to another. write and read function are called as a goroutine's hence, they will run concurrently. Once, write goroutine puts the message in the channel, read goroutine will read from it and print it in the console.

Blocking Operations

Channels operation i.e, read and write are blocking in nature.

  • When we receive data from a channel, It will block the goroutine until there is data available in the channel.
  • When we put data into a channel using write operation, it will block the goroutine until the data is consumed by another go routine.

If these operations are not handled correctly it will cause a deadlock.

Here's an simple example, run this and try to understand what's happening here

ouptut

In the above example, we have created a channel named ch in the main function. After creating a channel we are trying to put an integer value,i.e, 199 and read the integer from the channel, in subsequent lines. One might think the output of this code will be 199. But this results in a deadlock, because, as we discussed earlier, writing into a channel is blocking operation and there is no other goroutine waiting to read from the channel. main function will be blocked indefinitely. The main function will never reach the line where we are reading out from the channel. Hence, it will create a deadlock.

To overcome the deadlock in this particular case, one can create a new go routine and read the value from the channel in that goroutine instead of the main function. You can try editing the previous code to fix it.

We can use select statements to handle above case gracefully

Buffered Channels

By default, channels are unbuffered, which means a goroutine can send data into a channel, only if the previous data has been read out of the channel. This blocking nature can be inefficient in some cases, let's see an example to understand this better.

ouptut

In above example, We have two goroutines, producer and consumer. producer is putting integers 0 to 4, into the channel. consumer is reading integers from the channel and mimicking some expensive calculation by sleeping for 3 Millisecond. As we can see in the above example, the producer is being slowed down by the consumer as it's not able to consume the integers on time.

Buffered Channels allows us to specify a fixed length of buffer capacity so that it can store the values until another goroutine reads from it.

  • Write operation in a buffered channel is blocked when the buffer is full
  • Read operation is blocked when the channel is empty

We can create a buffered channel by using the make() function. We can pass the capacity of the buffered channel as a second parameter to the make function. The capacity of a buffered channel should be greater than 0 else it will be the same as an unbuffered channel.

buffCh := make(chan int, 5) 

Let's see how we can use the buffered channel in the previous example and improve it

ouptut

In above, code we have just made a one-line change while creating the channel, we added a buffer capacity of 3. Now, the producer will be able to complete its task and exist early because it doesn't have to wait for the slow consumer

Unidirectional Channels

By Default, Channels are bidirectional, i.e, you can write into and read out of the same channel. But you can create a channel that only supports sending or receiving data. To create a directed channel, we use the <- operator.

  • To declare a read-only channel, use the <- operator on the left of the chan keyword.
  • To declare a write-only channel, use the <- operator on the right of the chan keyword.
var ch <-chan int     // read-only channel

var ch chan<- int // write-only channel

Here's how we declare the unidirectional channel using the make function

ch := make(chan<- int)  // read-only channel 

ch := make(<-chan int) // write-only channel

We generally use the directed channels in the function parameter and return types. Often we will create bidirectional channels and then pass them to the function which requires a unidirectional channel. Go will convert the channels to unidirectional channels as required.

Here's an example showing the use case of a unidirectional channel

ouptut

In the above example, we have created a function name readData which takes a read-only chan of type interface{} and a context.Context. There is a for-select loop that tries to read from the dataCh channel or the ctx.Done() channel. If any data is available in the dataCh. It prints the data and its type. If there is anything in the ctx.Done() channel, it will return from the function.

In the main function, we have created a slice, data, of type interface{} with some values, a bidirectional channel, dataChannel, of type chan interface and context ctx, with a cancel function. We have forked a goroutine readData and passed the dataChannel and ctx. In a for loop, we are reading the data from the slice and passing it to the channel we had created earlier. Once we put all the values in the channel, we called the cancel() function so that we can exit the readData goroutine gracefully.

Note: If you try to read from a write-only channel or write to a read-only channel. It will result in an error.

Closing a channels

When we are working with channels, and one or more goroutines are waiting on the values from the channel, it's handy to be able to indicate that no more values will be sent over the channel. It helps the dependent goroutines to escape from blocking operations and move on to the next set of instructions. To close a channel we use the keyword close.

ch := make(chan string)   // create a channel named ch

close(ch) // close the channel

Read/Write operation in closed channel

If we try to write into a closed channel, it will panic the program. Let's try this with a simple example.

ouptut

As we can see in the above example, the program is panicking as we are trying to write over a closed channel. But, what if we try to read from the closed channel?

If we try to read from the closed channel, it will return a zero value of the type of channel.

ouptut

Note: <- operator optionally returns two values while trying to read from the channel. we can use the optional value to differentiate whether the read value from the channel was a value written by any other goroutine or generated by a closed channel.

Conclusion

In conclusion, channels play a critical role in concurrent programming in Go. In this article, we have covered the basics of channels, including how to create and use them, reading and writing data, the blocking nature of channel operations, the differences between buffered and unbuffered channels, and unidirectional channels, as well as the close operation.

The next article in this series will delve into the use of select statements in Go, which allow you to perform non-blocking communication on multiple channels. Select statements are a powerful tool for writing complex concurrent programs and make it easy to handle multiple channel events in a clean and organized manner.

In conclusion, this article provides a solid foundation for working with channels in Go. By understanding the concepts covered in this article, you will be well on your way to writing concurrent programs that are both robust and efficient. With the upcoming discussion of select statements, you will be able to take your Go programming skills to the next level!