Golang Channels - A Complete Guide
What is a channel?
A channel is a way for goroutines to communicate with each other. It is a typed conduit through which you can send and receive values with the channel operator,<-
. You can think of them like pipes that connect concurrent goroutines. They allow you to pass values between goroutines and synchronize their execution.
Creating a channel
Channels have a type associated with them i.e. chan T
where T
is the type of values the channel is allowed to transport. You can create a new channel with the make
built-in function:
This is an unbuffered channel of type int
. Unbuffered channels will block until a receiver is ready to receive the sent value.
Buffered Channels
A buffered channel is a channel that can hold a limited number of values. You can create a buffered channel by passing a second argument to the make
function:
This creates a channel that can hold up to 100 values of type int
. If you try to send more than 100 values into the channel, the send will block until there is room for the value.
Buffered channels are useful when you want to limit the number of goroutines that can access a resource or service at the same time. For example, if you want to limit the number of concurrent HTTP requests to a server, you can use a buffered channel to limit the number of goroutines that can access the server at the same time.
Sending and Receiving
You can send values into a channel using the channel operator, <-
. This sends the value on the left into the channel on the right:
In this example, we create a channel and send it the value 200
in a goroutine. Then we receive the value in the fmt.Println
statement.
Sending and Receiving from a Buffered Channel
Sending and receiving from a buffered channel works the same way as an unbuffered channel. The only difference is that a buffered channel will not block when sending a value if there is room in the buffer.
Closing a Channel
Channels are not closed by default. They need to be closed explicitly with the Close
method to indicate that no more values will be sent on the channel. This is important because it allows the receiving goroutine to know when the channel is empty and all values have been received and avoid any panic.
To avoid panic we can use the ok
idiom to check if the channel closed gracefully.
Select
Select is a control structure that allows a goroutine to wait on multiple communication operations. A select
blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.
Lets’s use an example with a context to understand how select works.
In this example, we use select
in the someFunc
function to wait for the context to be canceled. When the context is canceled, the Done
channel is closed and the select
statement will execute the case <-ctx.Done()
statement.
To explore more on contexts check out this article
Select with Default Case
The default case in a select is run if no other case is ready. This is useful for a non-blocking select that either does something or does nothing if none of the cases is ready.
In the first select
statement, there is no value ready to be received from the channel, so the default case is executed. In the second select
statement, there is a value ready to be received from the channel, so the case v := <-ch:
is executed.
Synchronization and Concurrency
Channels are a great way to synchronize and coordinate goroutines. This allows you to perform complex operations involving multiple inputs and outputs, without resorting to complex locking mechanisms such as mutexes.
One way to use channels to synchronize goroutines is to use a channel as a signal to indicate when a goroutine has finished. For example, if you have a function that takes a long time to run, you can use a channel to signal when it has finished.
In this example, a channel is used to create a blocking mechanism until the counter is incremented. This ensures that only one goroutine can increment the counter at a time thus preventing a race condition. The buffered channel has a capacity of 1, so only one goroutine can send a value into the channel at a time.
Another synchronization mechanism with channels was mentioned in the select section.
Advanced Channel Usage
Fan In and Fan Out Patterns
The fan-out and fan-in patterns are used to distribute work across multiple workers, then combine the results of the workers. This is a common pattern in concurrent programming when dealing with heavy workloads such as data processing.
In a fan-out pattern, you create a channel that will receive the workload, then create multiple workers that will read from the channel and perform the work. The workers will then write the results to a channel that will be used to combine the results.
In a fan-in pattern, you create a separate channel for receiving the results of each worker, then create a single goroutine that will read from the channels and combine the results.
Here’s an example that generates a series of random numbers, distributes them to multiple worker goroutines that square them, and finally combines the results:
Implementing pub-sub pattern
A pub-sub (Publisher-Subscriber) pattern allows multiple subscribers to receive updates from a single publisher. In Go, you can implement this pattern using a channel that represents the publisher’s output, and several channels that represent the subscribers’ inputs.
Here is an example that creates a publisher that sends random numbers every second, and three subscribers that receive the numbers and print them to the console:
Conclusion
Channels are a powerful tool for synchronizing and coordinating goroutines. In this article, we explored the basics of channels, and how to use them to synchronize and coordinate goroutines. We also looked at some advanced channel usage such as the fan-out and fan-in patterns, and the pub-sub pattern. By leveraging channels, you can create powerful concurrent programs that are easy to reason about.