Go EP5: Avoid context.Background(), Make Your Goroutines Promisable.
We'll talk about how to handle function returns smartly, filter data with zero waste, simplify complex decisions, and manage goroutines better.
Let’s recap what we’re talking about from the previous week:
Simplify Function Signatures with Structs or Variadic Options
Skip the ‘Get’ prefix for getters
Prefer ‘chan struct{}’ over ‘chan bool’ for signaling between goroutines.
Avoid Repetition in Naming
This week, we’re diving into four practical tips that can really sharpen your Go coding skills.
We’ll talk about how to handle function returns smartly, filter data with zero waste, simplify complex decisions, and manage goroutines better:
Take 1: Explicitly ignore values with a blank identifier (_) instead of silently ignoring them.
Take 2: Filter With Zero Allocation
Take 3: Creative way to convert multiple if-else statements into switch cases
Take 4: Avoid context.Background(), make your goroutines promisable.
Take 1: Explicitly ignore values with blank identifier (_) instead of silently ignoring them
When writing Go code, functions can return values that you may or may not want to use. In such cases, there are two approaches:
Implicitly: call the function without assigning its return value to any variable, this is short and concise.
Explicitly: a bit more verbose, explicitly ignore the return value by assigning it to the blank identifier _.
“Why is explicitly preferred when it’s more verbose and not as concise?”
In coding, clarity is always preferred over brevity.
This approach clearly indicates that we’re intentionally ignoring the return value of PerformOperation().
The use of _ =
signals to other developers (or to ourselves in the near future) indicates that the omission was deliberate and not an oversight.
“How about an error?”
Regardless, if a function returns an error, always handle it, or at the very least, log it. Also, consider adding a comment to explain why, for better clarity.
Take 2: Filter With Zero Allocation
When we filter slices in Go, the typical solution is to generate a new slice for the filtered elements.
This method, however, leads to additional memory allocation:
A smarter way to tackle this is by filtering the slice “in place," making use of the original slice’s underlying array.
Here’s how it works:
‘filtered := numbers[:0]’ creates a new slice ‘filtered’ that shares the underlying array with ‘numbers’, but starts with zero length while retaining numbers’s capacity.
As we append ‘num’ to ‘filtered’, we avoid extra memory allocations because we’re effectively modifying ‘numbers’ (or the underlying array of ‘numbers’).
So we’re not allocating new memory, we’re populating the existing array:
Keep in mind, this technique is best used when:
We no longer need ‘numbers’ after filtering.
And performance is critical, especially with large datasets.
Take 3: Creative way to convert multiple if-else statements into switch cases
It’s common to encounter complex conditional logic handled through multiple if-else statements:
This approach isn’t wrong.
But there’s a cleaner, more readable alternative: collapsing if-else chains into switch statements.
Now, first, we should understand how the switch-case structure works:
We can ignore ‘initialization’, and also ‘expression’. When we do this, we are essentially writing: switch {}
or switch true {}
, but the true is implicit.
Now, back to our example, let’s enhance it with what we just discussed:
Take 4: Avoid context.Background(), make your goroutines promisable.
We deal with goroutines for concurrent tasks, and these goroutines often need to perform blocking operations, like:
HTTP requests
Database queries and commands
Reading from and writing to channels
…
“Why avoid using context.Background() directly?”
We must ensure these operations don’t hang indefinitely or block the goroutine without an escape hatch to avoid resource leaks, unresponsive applications, deadlocks, etc.
There are two ways to make your goroutine promisable: cancel and timeout.
So, every goroutine you spawn is making a promise: “I will either complete my task or tell you why I couldn’t in time, and you can cancel my task anytime.”
Some key notes:
Under the hood, WithTimeout is WithDeadline in disguise.
Some XXXCause functions were just added in Go 1.20 and Go 1.21.
If a timeout occurs on XXXCause, it will provide a more detailed error message: “Context deadline exceeded: custom message.”
“How about channels? I don’t want to wait forever on a channel”
There are multiple ways to avoid waiting, but all use select{}:
There is a subtle note:time.After
leading to memory leaks (in the short term), consider usingtime.NewTimer(duration)
.