Skip to content

Latest commit

 

History

History
192 lines (140 loc) · 4.66 KB

5.2.md

File metadata and controls

192 lines (140 loc) · 4.66 KB

Channels

Concepts

The sleep in the previous section has many issues: It doesn't accurately describe the goal of waiting for all goroutines to exit before exiting, and it is inherently a race condition. Channels allow goroutines to be coordinated.

Channels allow you to send data between goroutines in a first-in first-out fashion.

First let's explore channels with the smallest goroutine possible, before jumping into more complex examples:

package main

import (
    "fmt"
    "time"
)

func main() {
    codes := make(chan int)
    func() {
        time.Sleep(4 * time.Second)
        codes <- 1
    }()
    code := <-codes
    fmt.Println(code)
}

Breaking this down line by line:

codes := make(chan int)

  • initializes codes as a unbuffered channel for ints.
  • We could not write var codes chan int because the zero value would be nil.

func() { ... }()

  • This is an anonymous closure.
  • codes <- 1 sends 1 to codes.
  • sends block until the receiver is ready to receive.

code := <-codes

  • This operation waits until codes has a value to give.
  • <-codes receives 1 from codes and initalizes code with it.
  • receives block until the sender is ready to send.

Stop and remove the anonymous function the pushes 1 to codes from program and re-run it. What happens?

fatal error: all goroutines are asleep - deadlock!

This tells us that code := <-codes is deadlocked because there are no goroutines to push data into codes.

Bufferred Channels

The difference between buffered channels and unbuffered channels is:

  1. There is no blocking on sends unless the buffer is completely full.
  2. There is no blocking on receives unless the buffer is empty.
  3. Buffered channels are initialized with make(chan type, N) with type being the data type of the channel, and N being the size of the buffer.

Let's recreate the previous example from 5.1 using buffered channels:

package main

import "fmt"

var saidHi chan bool = make(chan bool, 3)

func main() {
    for _, name := range []string{"Andrew", "Beavis", "Cory"} {
        go sayHi(name)
    }
    for i := 0; i < 3; i++ {
        <-saidHi
    }
}

func sayHi(n string) {
    fmt.Println(n)
    saidHi <- true
}

Directional Arguments

func someFunction(success chan bool, other string) {

This function signature takes a chan bool and a string, but doesn't describe how the channel is going to be used. We could make this function signature more descriptive by saying that success will only be sent to and never received from:

func someFunction(success chan<- bool, name string) {

To describe a channel that is only received from in a function signature, we could write <-chan bool.

Select statements

Select statements can be used to provide non-blocking channel operations:

select {
case code := <-codes:
    fmt.Println(code)
default:
    fmt.Println("No code received. Continuing")
}s

What happens when we would like to receive from multiple channels at once? You could create a goroutine for each one, but sometimes that causes explosive complexity. You can also use select for this:

select {
case code := <-codes:
    fmt.Println(code + 1)
case msg := <-messages:
    fmt.Println(msg)
}

The above example is blocking until one of codes or messages sends a value. If default is added, it would be non-blocking.

close()

Closing a channel indicates no more data is going to be sent over it:

package main

import "fmt"

var data = make(chan bool, 2)

func main() {
    go AddValues()
    for {
        element, more := <-data
        if more {
            fmt.Println(element)
        } else {
            break
        }
    }
    fmt.Println("complete")
}

func AddValues() {
    data <- true
    data <- false
    close(data)
}

The important pieces from above:

element, more := <-data

  • Similar to type casting or map indexing, we can include a second variable more to determine if the channel is closed.

close(data)

  • This indicates that no more data will be sent.

The last thing that close empowers you to do is use channels with range:

package main

import "fmt"

func main() {
    x := make(chan bool, 5)
    x <- false
    x <- true
    close(x)
    for el := range x {
        fmt.Println(el)
    }
}

Exercises

Tips

While important, goroutines and channels add complexity and make debugging much harder. Ensure that the concurrency fits the problem before diving into a solution.

Further Reading

Memory leaks? Totally possible with goroutines and channels.


prev -- up -- next