A strong and idiomatic way for goroutines to communicate and synchronize with one another is through channels. Channels give these activities a secure and coordinated means of exchanging data, whilst goroutines enable the execution of jobs concurrently. The phrase “Don’t communicate by sharing memory, share memory by communicating” is frequently used to sum up the fundamental idea of Go’s concurrency. The main means of accomplishing this are channels.
What are Channels in Go?
Between two or more goroutines, a channel serves as a communication pipe that enables them to securely transfer values of a particular kind. By ensuring that only one goroutine has access to a certain piece of data at any given time, frequent concurrency problems like race situations are avoided.
Characteristics of channels
Typed: A particular data type is specified when the channel is constructed and is intended to be transmitted by each channel (e.g., chan int for integers, chan string for strings).
Creation: Channels, like maps and slices, are made with the built-in build function. Consider the following: c := make(chan int).
Send and Receive Operations: The operator <- (left arrow) is used to send and receive messages on a channel.
- Sending: A value (data) can be sent to a channel (c) by pointing the arrow in its direction: c – data.
- Receiving: The arrow points away from the channel as v := <-c, or simply <-c, to receive a value from a channel (c) and optionally store it in a variable (v).
Blocking Behavior: Sending to a channel will automatically block until a different goroutine is prepared to receive the message, and receiving from a channel will do the same until a sender is prepared to transmit. Goroutine execution is synchronised by this built-in blocking behaviour. Memory is the only resource used by a blocked goroutine.
Program Termination: Every other goroutine that is running is stopped if the main goroutine terminates. Channels are frequently used to make sure that the main goroutine doesn’t depart until other goroutines have finished their work.
Basic Channel Communication Code Example
This is an example showing how a channel can be used to communicate between two goroutines. The “ping” messages are sent by the pinger goroutine, received by the printer goroutine, and printed.
package main
import (
"fmt"
"time"
)
// pinger sends "ping" messages on the channel c indefinitely.
func pinger(c chan string) {
for i := 0; ; i++ { // Infinite loop
c <- "ping" // Send "ping" to the channel. This will block until printer is ready.
}
}
// printer receives messages from the channel c and prints them.
func printer(c chan string) {
for { // Infinite loop
msg := <-c // Receive a message from the channel. This will block until pinger sends a message.
fmt.Println(msg)
time.Sleep(time.Second * 1) // Simulate work
}
}
func main() {
var c chan string = make(chan string) // Create an unbuffered channel of strings
go pinger(c) // Start pinger as a goroutine
go printer(c) // Start printer as a goroutine
// Keep the main goroutine alive indefinitely to allow pinger and printer to run
var input string
fmt.Scanln(&input) // This line waits for user input, preventing main from exiting prematurely
}
Output
ping
ping
ping
ping
ping
ping
ping
ping
ping
ping
The pinger and printer in this example use channel c to synchronize their operations. Blocking occurs when a pinger tries to send and waits for the printer to be ready to receive (and vice versa).
Channel Direction
As a function parameter, a channel type can be restricted to either transmitting or receiving by specifying a direction. Because unintentional usage is avoided, programs become more reliable and secure.
chan<- Type
: Channel that is send-only. This channel is the sole one to receive values. There will be a compile-time error if you try to receive from it.
<-chan Type
: A channel that can only be received. Only through this route can values be obtained. If you try to close it or send to it, a compile-time error will occur.
When these limitations are removed, a channel is bidirectional. It is not possible to pass a bidirectional channel to a function that anticipates a send-only or receive-only channel.
Channel Direction Example
package main
import (
"fmt"
"time"
)
// pinger can only send strings to the channel c
func pinger(c chan<- string) {
for i := 0; ; i++ {
c <- "ping"
time.Sleep(time.Second) // Added sleep for clearer output
}
}
// ponger can only send strings to the channel c
func ponger(c chan<- string) {
for i := 0; ; i++ {
c <- "pong"
time.Sleep(time.Second) // Added sleep for clearer output
}
}
// printer can only receive strings from the channel c
func printer(c <-chan string) {
for {
msg := <-c
fmt.Println(msg)
time.Sleep(time.Second * 1)
}
}
func main() {
c := make(chan string) // c is a bidirectional channel
go pinger(c) // Pass c to pinger (send-only context)
go ponger(c) // Pass c to ponger (send-only context)
go printer(c) // Pass c to printer (receive-only context)
var input string
fmt.Scanln(&input) // Keep main alive
}
Output
ping
pong
ping
pong
ping
pong
ping
pong
ping
pong
ping
pong
ping
pong
This program will alternate between writing “ping” and “pong” when the printer routine gets messages from the sender (ponger or pinger) that is ready.
select
Statement
Select, a special statement offered by Go, functions similarly to a switch statement but for channels. It enables a goroutine to await several communication actions (receives or sends).
- The first channel that is prepared for an operation is chosen by choose, which then runs the relevant case block.
- If there are several channels available, choose one at random to start with.
- The choose statement pauses until a channel opens up if none are available.
- You can include a default case in a choose statement. In the event that no other case is prepared, the default case runs right away without blocking. This makes non-blocking channel operations possible.
- Time.When timeouts are implemented in select statements, after is helpful since it returns a channel that delivers the current time after a predetermined amount of time.
Statement Code Example (with Timeout)
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
for {
c1 <- "from 1"
time.Sleep(time.Second * 2) // Sends every 2 seconds
}
}()
go func() {
for {
c2 <- "from 2"
time.Sleep(time.Second * 3) // Sends every 3 seconds
}
}()
go func() {
for {
select {
case msg1 := <-c1: // Try to receive from c1
fmt.Println(msg1)
case msg2 := <-c2: // Try to receive from c2
fmt.Println(msg2)
case <-time.After(time.Second * 4): // Timeout after 4 seconds if no channel is ready
fmt.Println("timeout occurred for an operation")
// This case will be chosen if both c1 and c2 are not ready for 4 seconds.
// However, given the send frequencies above, one of them will typically be ready sooner.
}
}
}()
var input string
fmt.Scanln(&input) // Keep main alive
}
Output
from 2
from 1
from 1
from 2
from 1
from 2
from 1
from 1
from 2
Printing “from 1” every two seconds and “from 2” every three seconds is how this software operates. By selecting the first message that becomes available, the select statement coordinates these actions.
Buffered Channels
The sender and the recipient must be prepared for communication to take place because channels are often synchronous and unbuffered (blocking). By passing a second integer parameter to the make function that indicates the channel’s capacity, you can, nevertheless, establish a buffered channel.
- Create(chan int, 1) c := generates a channel buffer with a capacity of one.
- The buffer must be filled before sending to a buffered channel will block.
- If a buffered channel is not empty, receiving from it won’t block.
- Buffered channels, which function like semaphores, can be used to restrict an application’s throughput. They help handle unexpected spikes in data and offer a queue for unfinished tasks.
Buffered Channel Code Example
package main
import "fmt"
func main() {
// Create a buffered channel with a capacity of 2
messages := make(chan string, 2) //
// Sending to the buffered channel. These sends won't block immediately
// because the buffer has capacity.
messages <- "buffered" // Buffer has 1 item
messages <- "channel" // Buffer has 2 items
// If we tried to send a third message here (messages <- "full"),
// it would block until a receiver reads a message, because the buffer would be full.
// fmt.Println(len(messages)) // You can check the current number of elements in the buffer
// Receiving from the buffered channel.
fmt.Println(<-messages) // Reads "buffered"
fmt.Println(<-messages) // Reads "channel"
// If we tried to receive a third message here (<-messages),
// it would block until a sender puts a message into the channel, because the buffer is empty.
fmt.Println("Done")
}
Output
buffered
channel
Done
This example shows that, provided the buffer is not full, messages can be transmitted to the channel even in the absence of an immediate recipient.
Nil Channels
A nil channel is a unique type of channel that will always prevent any send or receive operations on it. Using this property, you can set a channel variable to nil to temporarily disable a case in a select statement.
You can also read Goroutines In GoLang: A Deep Dive Into Concurrency Model