Goroutines & Channels

Goroutines

Finally, we get to the reason we're using Go for this part of the course: concurrency.

A goroutine is a function call that is run concurrently with the code that calls it, and any other goroutines.

To create a goroutine, just make a function call with the go keyword before it:

go doWork(x)
doOtherWork()

Goroutines

A goroutine is a user thread: it's cheap to create and having thousands of them is okay.

The Go runtime environment takes care of creating goroutines, and running them in kernel threads to get parallelism. It will make sure there are enough kernel threads. (i.e. Go uses hybrid threading.)

Goroutines

A real example:

func Count(ident string, n int) {
	for i := 1; i <= n; i++ {
		fmt.Printf("%s%d ", ident, i)
		time.Sleep(50 * time.Microsecond)
	}
}

func testGoroutines1() {
	go Count("a", 5)
	go Count("B", 4)
	fmt.Scanln() // wait so we can see all goroutines finish
}

Goroutines

The goroutines are truly running concurrently, with all of the uncertainty that comes with that. I saw these outputs in three consecutive runs:

a1 B1 a2 B2 a3 a4 B3 B4 a5
a1 B1 a2 B2 a3 B3 B4 a4 a5
a1 B1 B2 a2 a3 B3 a4 a5 B4

Goroutines

It's also possible to have a goroutine run an anonymous function:

a := 3
go func(n int) {
	fmt.Println(n)
}(a)

There's a lot of syntax there. The pieces:

  • func(…) { … }: anonymous function definition.
  • func(…) { … }(a): call the anonymous function.
  • go func(…) { … }(a): run in a goroutine.

Goroutines

An anonymous function is only going to be readable if it's very short and simple. Otherwise, I suggest giving it a name.

Goroutines

Anonymous functions are closures in Go. Variables in scope when it's defined are captured.

b := 5
go func(c int) {
	fmt.Println(b + c)
}(6)
11

Channels

Goroutines are good at doing things (all of those just printed), but what if we want to return a value? Where does the return value go here?

func addBytes(a, b byte) byte {
	return a + b
}
go addBytes(3, 4)

Nowhere (as far as I can tell). If we want values back from a goroutine, we need something else.

Channels

A channel is a (thread-safe) communications channel where you can send and recieve values of any type. Basics…

Create a channel with make, giving the type it will send/​receive. e.g. a channel to transmit int values:

ch := make(chan int)

Channels

Send a message (of the correct type) to the channel with the <- operator:

ch <- 383

And receive a message (in another thread) with the same operator on the other side of the channel name:

result := <-ch

Channels

A complete (but minimal) example. For a function to send/​receive on a channel, we pass it in as an argument:

func addBytesChannel(a, b byte, result chan byte) {
	result <- a + b
}

Call it by passing the channel, and then waiting for a result to arrive:

result := make(chan byte)
go addBytesChannel(3, 4, result)
res := <-result
fmt.Println(res)

Channels

A less silly example: do some calculations on an array in parallel and then collect the results (in whatever order they arrive).

func countTransformed(values []float64) int {
	transformed := make(chan float64)
	for _, v := range values {
		go transformingCalculation(v, transformed)
	}
	count := 0
	var tv float64
	for range values { // iterate len(values) times
		tv = <-transformed
		if tv > 0 {
			count += 1
		}
	}
	return count
}

Channels

In that example, we assume that each goroutine will send exactly once to the channel, so there will be exactly len(values) messages coming back on the channel.

The function assumes that it can receive that many times from the channel. If not, it will deadlock.

Channels

The results may come back in any order from the channel. In this case, the logic doesn't care, but in general it might be a problem.

That code creates one goroutine for each array element: if the array is big, that might be unreasonable. If the calculation is short, it might be wasteful.

Channel Range

We have seen range to iterate over values in an array. It can also iterate over values from a channel.

But, it has to know when there are no more values to receive. A channel can be closed to indicate that no more values will be sent.

// Send up to n random values to the channel.
func RandRand(n int, values chan float32) {
	nValues := rand.Intn(n) + 1
	for i := 0; i < nValues; i++ {
		values <- rand.Float32()
	}
	close(values)
}

Channel Range

Then a for/range will iterate through the values (and then exit the loop).

func PrintRands() {
	randValues := make(chan float32)
	go RandRand(5, randValues)
	for v := range randValues {
		fmt.Printf("%v ", v)
	}
	fmt.Println("done")
}

Output will look like:

0.6109869 0.6371221 0.27911648 done

Channel Range

Closing the channel is necessary here to indicate that the forrange should exit. Otherwise, it's not necessary to close().

Buffered Channels

By default, channels block. That is, when code sends with ch<-, it will wait at that line until some other goroutine does <-ch to receive it.

Sometimes that's not okay: you might need to send a message and move on with other work, even if it isn't going to be received immediately.

Buffered Channels

e.g. here, all of the goroutines have to wait around until their message is received.

ch := make(chan int)
for i := 0; i < 3; i++ {
	go func() {
		fmt.Println("about to send")
		ch <- 1
		fmt.Println("done sending")
	}()
}
time.Sleep(100 * time.Millisecond)
for i := 0; i < 3; i++ {
	fmt.Println("got message:", <-ch)
}

Buffered Channels

We can create a buffered channel that has some capacity to hold messages until they are received.

In the example, we can change the channel creation and add a buffer length:

ch := make(chan int, 3)

Buffered Channels

The output without buffering (left) and with (right):

about to send
about to send
about to send
got message: 1
got message: 1
got message: 1
done sending
done sending
done sending
about to send
done sending
about to send
done sending
about to send
done sending
got message: 1
got message: 1
got message: 1

With the buffer, these goroutines can exit (or move on to other calculations).

Channel Select

What if we have more than one channel to worry about?

The select statement lets you listen for any one of a collection of channels. The syntax is almost exactly like the switch statement:

select {
case x := <-channelX:
    fmt.Println(x)
case y := <-channelY:
    fmt.Println(y)
case z := <-channelZ:
    fmt.Println(z)
}

Channel Select

A slightly more realistic example: we want to process values from two streams that are generated concurrently: [full code]

squares := make(chan int)
roots := make(chan float64)
go GenerateSquares(10, squares)
go GenerateSquareRoots(10, roots)
for i := 0; i < 20; i++ {
	select {
	case sq := <-squares:
		total += float64(sq)
	case rt := <-roots:
		total += rt
	}
}

Channel Select

The idea: any channel that has a message to receive (buffered or not) can let the select statement continue. We can deal with multiple streams of data as they arrive.

Concurrency Plan

My plan for Go + concurrency: that is enough basics to see how the language deals with concurrency. There is a lot more to say about how to deal with the tools the language gives us.

I'm going to pause concurrency for now and come back later.