Golang Concurrency Jump Start
Tags: go, tutorial, programming, concurrency
Published: 2022-07-20 03:55:14

## Takeaways In This Post

- Only store primitives and immutable or read only values in Contexts
- Do not use Context to store application logic values
- Always create a main cancellable Context to allow goroutines to gracefully stop
- Check Context cancellation before and during long running tasks
- Use channels to logically partition concurrent work
- [Do not communicate by sharing memory; instead, share memory by communicating](https://go.dev/blog/codelab-share)

## Context
A [context.Context](https://pkg.go.dev/context#Context) is a reference (because it is an interface) to an immutable key/value pair that is one node in a tree of Contexts. Each Context can lookup values using the recursive function `Value(key) any` and check cancellation of the chain with `Done() <-chan struct{}`. This structure allows values and cancel signals to be shared amongst numerous goroutines. This allows data to be shared safely. One major use case is profiling.

### Common Context Pitfalls
Context is commonly abused and misused by allowing values stored in the Context to be mutable and also by storing Contexts inside structs. Both of these mistakes lead to potential race conditions and other issues. Avoid storing maps, slices, references, and channels as Context values. Stick to primitives and struct values. Beware of references stored in struct values as those may also present race condition issues.

_Takeaway: Only store primitives and immutable or read only values in Contexts_

Another issue is the tendency to use Context as a grab bag of application values. Keep in mind that the more values stored in the Context the longer look ups become, `O(n)`. This pattern also makes debugging difficult since all of the values are "hidden". Pass the values you require as arguments.

_Takeaway: Do not use Context to store application logic values_

A good practice to follow is always create an application Context before anything else. This allows the main to gracefully shutdown any running goroutines before exiting. Otherwise, goroutines will be halted mid-execution without any opportunity to stop.

## Hello, Concurrency!
Basic "Hello, World!" main for concurrency.

```golang
func main() {
    ctx, cancel := context.WithCancel(context.Background())

    // Do parallel work...
    go task(ctx)

    cancel()
    time.Sleep(5 * time.Second)
}
```
[goplay](https://goplay.tools/snippet/IW85rmJcLuQ)

There are improvements that should be done, but this will allow you to spawn any number of goroutines and allow five seconds for those to stop.

_Takeaway: Always create a main cancellable Context to allow goroutines to gracefully stop_

Of course, goroutines do not simply stop by themselves. They have to monitor their Context for cancellations. A good way to do this is using `select`. This will block until one of the `case` branches becomes available.

```golang
func task(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            // Context cancelled. Should now stop processing work.
            return
        default:
            // Process work
        }
    }
}
```
[goplay](https://goplay.tools/snippet/8wUNUCyIyCi)

This function will run until its Context is cancelled. Because `default` is always available, this will check the Done() channel for a value first and if nothing is ready then it will execute the `default` branch. Even if a task is not continuously running, it is always a good idea to check the Context before performing an expensive operation, such as a [database query](https://github.com/go-redis/redis/blob/e061db8c13fd6b5e007d73370873026e360719de/redis.go#L293).

_Takeaway: Check Context cancellation before and during long running tasks_

## Data Concurrency
When data needs to be accessed by the same goroutine, it is very easy to wrap that data in a `sync.Mutex` and call it a day. This can lead to complex code and deadlocks. Golang provides a better way of communicating data between goroutines in the form of channels. You can think of channels like a hand off of data from one goroutine to another. That hand off blocks on both ends until both sides are ready. 

This pattern does a few things for you:
- Guarantees only one process is modifying data at a time
- Provides logical partitions of work that are easy to understand
- Decouples code that would otherwise rely on specific mutexes

```golang

func readPacket(ctx context.Context, packetChannel chan Packet) {
    for {
        select {
        case <-ctx.Done():
            // Context cancelled. Should now stop processing work.
            return
        case packet, ok := <-packetChannel:
            if !ok {
                return
            }

            packet.ReadCount++
            packetChannel <- packet
        }

        time.Sleep(time.Millisecond)
    }
}
```
[goplay](https://go.dev/play/p/aNUnIjIPuNz)

`readPacket()` continuously checks the channel for new Packets, reads it, and then dumps it back onto the channel for another goroutine to process. If the channel is ever closed then we should stop. `ok` becomes false once this happens. The small sleep in such a fast looping goroutine like this one allows time for other goroutines to process and prevents the CPU from maxing. 

Instead of taking a Packet blocking on mutex associated with it, we can freely pass the Packet between goroutines. The goroutine is an isolated slice of work to be done on the Packet. There may be other operations performing work on it in other goroutines, but those would never impact the thread safety of this goroutine task.

_Takeaway: Use channels to logically partition concurrent work_

_Takeaway: Do not communicate by sharing memory; instead, share memory by communicating_