Go Concurrency

Posted on Jul 31, 2021

I’m working on a personal project in Go at the moment, and am looking at concurrency, so here are some basic tl;dr notes on this talk by Rob Pike.

Concurrency is the composition of independently executing computations - a way to write clean code that interacts well with the real, and very concurrent, world. It is not parallelism. If your computer only has one processor, it cannot be parallel because it is only executing one instruction at a time. But it can still be concurrent.

Goroutines let you run a function in the background. When main() returns, the program exits.

A goroutine is an independently executing function, launched by a go statement. It has its own stack, which grows and shrinks as required. They start very small, so it is practical and cheap to have even millions of goroutines running.

A channel allows communications between goroutines. They can be created like so:

// Declaring and initialising.
c := make(chan int)

// Sending on a channel.
c <- 1

// Receiving from a channel.
// The arrow indicates the direction of data flow.
value = <-c

Follow the arrows - when sending, the 1 goes into the channel, and when receiving, the 1 comes out of the channel and is assigned to value.

Goroutines communicate and synchronise with one another. Buffered channels allow a way to drop a value to a buffer and continue.

Go approach to concurrency: don’t communicate by sharing memory, share memory by communicating

i.e. don’t share a blob of memory and protect it from parallel access, just pass the data around over the channels between goroutines.

Patterns

Generator: a function that returns a channel

You can create a function that returns a receive-only channel like so:

func example(input string) <-chan string { // Returns reveive-only channel of strings.

	//function

return c // Returns the channel to the caller. 
}

You can create multiple channels but they will be in lockstep, i.e. each will have to wait for the next to send data on the channel. If you have functions with different levels of verbosity, you can use the fanIn or Multiplexer function:

// In main():
c := fanIn(example("ex1"), example("ex2"))

// The fanIn function
func fanIn(input1, input2 <-chan string) <-chan string {
	c := make(chan string)
	go func() { for { c <- <-input1 } }()
	go func() { for { c <- <-input2 } }()
	return c
}

This will take any input from either function and forward it on.

Restoring sequencing

You can send a ‘signal’ channel within a channel to implement lockstep waiting between goroutines.

type Message struct {
	str string
	wait chan bool // A return communication channel used to signal wait status.
}

Select

Select statements are like switches but they evaluate different communication channel status. It blocks until one of its cases can run, then executes the case. If multiple cases are ready, it chooses at random> Here’s an example from the Go docs:

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

You can use time.After() to timeout connections. It returns a channel that will deliver a value after a certain interval.

Towards the end of the video, there are some examples of how you can use concurrency and replication to make efficient processes, for example duplicating backend processes and choosing the one that responds first.