DEV Community

Cover image for Go (11) - Channels & Concurrency
Chathumi Kumarapeli
Chathumi Kumarapeli

Posted on

Go (11) - Channels & Concurrency

Concurrency VS Synchronization

Synchronous code

When code executes line by line in order, one thing at a time is called synchronous. It is simple, but sometimes might not be very efficient.

Concurrency

func main() {
    // block 1
    x := 10
    x++
    fmt.Printf("x = %d", x)

    // block 2
    y := 20
    y--
    fmt.Printf("y = %d", y)
}
Enter fullscreen mode Exit fullscreen mode

Check the code blocks in the above example. As you can see, the code lines in block 1 should execute in order. The code lines in block 2 should also execute line by line. But blocks 1 and 2 have no dependency on each other. So, we can run them separately on two CPU cores. This is called running in Parallel. It increases efficiency.

In Golang, it is easy to write concurrent code.

Go routine

func main() {
    // some code
    go funcName()

    // rest of the code
}
Enter fullscreen mode Exit fullscreen mode

In the above example, you can see go funcName(). It tells that the funcName() function runs in parallel with the rest of the code in the main function.
So all you need to do is use the go keyword to instruct it to run in concurrent mode. We have a special name here for concurrent functions: go routine. In the above example, funcName() is a go routine.

We can't expect return values from a go routine like we get from a regular function.

Now, you might wonder what the use of these go routines is if they can't even return a value. That is why we have channels.

Channels

Channels are used to communicate between goroutines. They are a type-safe and thread-safe queue.
One or more goroutines can put data into a channel, and one or more other goroutines read that data from the channel.

Syntax to make a channel,

ch := make(chan int)
Enter fullscreen mode Exit fullscreen mode

Above, initialize a channel of type int.

You can put data into channels using the syntax below.

ch <- 10
Enter fullscreen mode Exit fullscreen mode

The <- is called the channel operator. The arrowhead shows the data flow direction.

Use the below syntax to receive data from a channel.

val := <- ch
Enter fullscreen mode Exit fullscreen mode

It removes the value from the channel and saves it to the val variable.

Both sending and receiving data operations are blocking operations. If a value is sent to a channel but there is no other goroutine to receive the data, the code will stop and wait. The same happens during the reading. When a goroutine is expecting data from a channel, but no one is writing data to it, then the code is blocked there.

A token is a unary value. Most of the time, empty structs are used as tokens. What is passed through the channel is not a concern in this scenario. <-ch syntax is used to block and wait until something is sent to a channel.

Buffered Channels

When making a channel, we can make it buffered by providing a buffer length as the second argument.

ch := make(chan int, 100)
Enter fullscreen mode Exit fullscreen mode

When the buffer is full, sending to the channel gets blocked. Likewise, when the buffer is empty, the receiving from the channel is blocked.

Closing channels

Channels should always be closed from the sending side. It says that this channel is closed, there's nothing more to read from it. The syntax is given below.

ch := make(chan int)
// code lines ...
close(ch)
Enter fullscreen mode Exit fullscreen mode

Therefore, on the reading side, we have to check whether a channel is closed.

v, ok := <-ch
Enter fullscreen mode Exit fullscreen mode

In the above example, the ok is a boolean variable. Its value is true if the channel is open. Else, ok is false.

If you send a value to a closed channel, that go routine will panic.

Closing a channel is not necessary. Unused open channels are garbage collected.

Range

The range can be used on channels as well. The example below receives values over the channel until it is closed. When the channel is closed, the loop exists.

    for item := range ch {
        // item is the next value received from the ch
    }
Enter fullscreen mode Exit fullscreen mode

Select

When there is a single goroutine listening to multiple channels, we might need to process data in order per channel. In this case, we can use the select statement to switch between channels.

select {
    case i, ok := <- chInst:
        if !ok {
            return
        }
        fmt.Print(i)
    case s, ok := chString:
        if !ok {
            return
        }
        fmt.Print(i)
}
Enter fullscreen mode Exit fullscreen mode

Read-only channels

When you want to make a channel read-only, you should cast it from chan to <-chan type.

func readCh(ch <-chan int) {
    // ch is a read-only channel in this function
}
Enter fullscreen mode Exit fullscreen mode

Write-only channels

To make a channel write-only, you should cast it from chan to chan<- type.

func readCh(ch chan<- int) {
    // ch is a write-only channel in this function
}
Enter fullscreen mode Exit fullscreen mode

Read-only and write-only channels concept is useful in identifying which functions use the channel for reading and which use it for writing.

Few points to remember:

  • A send to a nil channel blocks the code forever
  • A receive from a nil channel blocks forever
  • A send to a closed channel panics
  • A receive from a closed channel returns the zero value

Top comments (0)