Channels

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.

Note

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

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.

The send and receive operations

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

intCh <- 12

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, 12 is sent into channel intCh.

value := <- intCh

Receive

When the <- operator is place to the left of a channel, it indicates a receive operation from the channel. The value variable is assigned the value received from the intCh channel.

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.

Unbuffered channel

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:

Unbuffered channel

The sequence in the preceding figure (from left to right) shows how the unbuffered channel works:

  • If the channel is empty, the receiver blocks until there is data
  • The sender can send only to an empty channel and blocks until the next receive operation
  • When the channel has data, the receiver can proceed to receive the data.

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.

Buffered channel

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:

Buffered channel

The buffered channel depicted in the preceding figure has the following characteristics:

  • When the channel is empty, the receiver blocks until there is at least one element
  • The sender always succeeds as long as the channel is not at capacity
  • When the channel is at capacity, the sender blocks until at least one element is received

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.

Unidirectional channels

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

<- chan <element type>

Declares a receive-only channel as shown later.

var Ch <-chan int

chan <-<element type>

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.

Channel length and capacity

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.

Note

An unbuffered channel has a length and a capacity of zero.

Closing a 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:

  • Subsequent send operations will cause a program to panic
  • Receive operations never block (regardless of whether buffered or unbuffered)
  • All receive operations return the zero value of the channel's element type

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.14.151.45