When talking about concurrency, one of the natural concerns that arises is that of data safety and synchronization among concurrently executing code. If you have done concurrent programming in languages such as Java or C/C++, you are likely familiar with the, sometimes brittle, choreography required to ensure running threads can safely access shared memory values to achieve communication and synchronization between threads.
This is one area where Go diverges from its C lineage. Instead of having concurrent code communicate by using shared memory locations, Go uses channels as a conduit between running goroutines to communicate and share data. The blog post Effective Go (https://golang.org/doc/effective_go.html) has reduced this concept to the following slogan:
Do not communicate by sharing memory; instead, share memory by communicating.
The concept of channel has its roots in communicating sequential processes (CSP), work done by renowned computer scientist C. A. Hoare, to model concurrency using communication primitives. As will be discussed in this section, channels provide the means to synchronize and safely communicate data between running goroutines.
This section discusses the Go channel type and provides insights into its characteristics. Later, you will learn how to use channels to craft concurrent programs.
The channel type declares a conduit within which only values of a given element type may be sent or received by the channel. The chan
keyword is used to specify a channel type, as shown in the following declaration format:
chan <element type>
The following code snippet declares a bidirectional channel type, chan int
, assigned to the variable ch
, to communicate integer values:
func main() { var ch chan int ... }
Later in the chapter, we will learn how to use the channel to send data between concurrent portions of a running program.
Go uses the <-
(arrow) operator to indicate data movement within a channel. The following table summarizes how to send or receive data from a channel:
Example |
Operation |
Description |
|
Send |
When the arrow is placed to the left of the value, variable or expression, it indicates a send operation to the channel it points to. In this example, |
|
Receive |
When the |
An uninitialized channel has a nil zero value and must be initialized using the built-in make function. As will be discussed in the following sections, a channel can be initialized as either unbuffered or buffered, depending on its specified capacity. Each of type of channel has different characteristics that are leveraged in different concurrency constructs.
When the make
function is invoked without the capacity argument, it returns a bidirectional unbuffered channel. The following snippet shows the creation of an unbuffered channel of type chan int
:
func main() { ch := make(chan int) // unbuffered channel ... }
The characteristics of an unbuffered channel are illustrated in the following figure:
The sequence in the preceding figure (from left to right) shows how the unbuffered channel works:
Sending to an unbuffered channel can easily cause a deadlock if the operation is not wrapped in a goroutine. The following code will block after sending 12
to the channel:
func main() { ch := make(chan int) ch <- 12 // blocks fmt.Println(<-ch) }
golang.fyi/ch09/chan-unbuff0.go
When you run the previous program, you will get the following result:
$> go run chan-unbuff0.go fatal error: all goroutines are asleep - deadlock!
Recall that the sender blocks immediately upon sending to an unbuffered channel. This means any subsequent statement, to receive from the channel for instance, becomes unreachable, causing a deadlock. The following code shows the proper way to send to an unbuffered channel:
func main() { ch := make(chan int) go func() { ch <- 12 }() fmt.Println(<-ch) }
golang.fyi/ch09/chan-unbuff1.go
Notice that the send operation is wrapped in an anonymous function invoked as a separate goroutine. This allows the main
function to reach the receive operation without blocking. As you will see later, this blocking property of unbuffered channels is used extensively as a synchronization and coordination idioms between goroutines.
When the make
function uses the capacity argument, it returns a bidirectional buffered channel, as shown in the following snippet:
func main ch := make(chan int, 3) // buffered channel }
The previous code will create a buffered channel with a capacity of 3
. The buffered channel operates as a first-in-first-out blocking queue, as illustrated in the following figure:
The buffered channel depicted in the preceding figure has the following characteristics:
Using a buffered channel, it is possible to send and receive values within the same goroutine without causing a deadlock. The following shows an example of sending and receiving using a buffered channel with a capacity of 4
elements:
func main() { ch := make(chan int, 4) ch <- 2 ch <- 4 ch <- 6 ch <- 8 fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) }
golang.fyi/ch09/chan0.go
The code in the previous example is able to send the values 2
, 4
, 6
, and 8
to the ch
channel without the risk of blocking. The four fmt.Println(<-ch)
statements are used to receive the values buffered in the channel successively. However, if a fifth send operation is added, prior to the first receive, the code will deadlock as highlighted in the following snippet:
func main() { ch := make(chan int, 4) ch <- 2 ch <- 4 ch <- 6 ch <- 8 ch <- 10 fmt.Println(<-ch) ... }
Later in the chapter, you will read more about idiomatic and safe ways to use channels for communications.
At declaration, a channel type may also include a unidirectional operator (using the <-
arrow again) to indicate whether a channel is send-only or receive-only, as listed in the following table:
Declaration |
Operation |
|
Declares a receive-only channel as shown later. var Ch <-chan int |
chan |
Declares a send-only channel as shown later. var Ch <-chan int |
The following code snippet shows function makeEvenNums
with a send-only channel argument of type chan <- int
:
func main() { ch := make(chan int, 10) makeEvenNums(4, ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) } func makeEvenNums(count int, in chan<- int) { for i := 0; i < count; i++ { in <- 2 * i } }
golang.fyi/ch09/chan1.go
Since the directionality of the channel is baked in the type, access violations will be detected at compile time. So in the previous example, the in
channel can only be used for receive operations.
A bidirectional channel can be converted to a unidirectional channel explicitly or automatically. For instance, when makeEvenNums()
is called from main()
, it receives the bidirectional channel ch
as a parameter. The compiler automatically converts the channel to the appropriate type.
The len
and cap
functions can be used to return a channel's length and capacity respectively. The len
function returns the current number of elements queued in the channel prior to being read by a receiver. For instance, the following code snippet will print 2:
func main() { ch := make(chan int, 4) ch <- 2 ch <- 2 fmt.Println(len(ch)) }
The cap
function returns the declared capacity of the channel type which, unlike length, remains constant throughout the life of the channel.
Once a channel is initialized it is ready for send and receive operations. A channel will remain in that open state until it is forcibly closed using the built-in close function, as shown in the following example:
func main() { ch := make(chan int, 4) ch <- 2 ch <- 4 close(ch) // ch <- 6 // panic, send on closed channel fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) // closed, returns zero value for element }
golang.fyi/ch09/chan2.go
Once a channel is closed, it has the following properties:
In the previous snippet, the ch
channel is closed after two send operations. As indicated in the comment, a third send operation would cause a panic because the channel is closed. On the receiving side, the code gets the two elements in the channel before it is closed. A third receive operation returns 0
, the zero value for the channel's elements.
Go offers a long form of the receive operation that returns the value read from the channel followed by a Boolean indicating the closed status of the channel. This can be used to properly handle the zero value from a closed channel, as shown in the following example:
func main() {
ch := make(chan int, 4)
ch <- 2
ch <- 4
close(ch)
for i := 0; i < 4; i++ {
if val, opened := <-ch; opened {
fmt.Println(val)
} else {
fmt.Println("Channel closed!")
}
}
}
golang.fyi/ch09/chan3.go
52.14.151.45