avatarPhuong Le (@func25)

Summary

The article discusses advanced techniques in Go programming, focusing on context management, error handling, and code readability, with an emphasis on the new features introduced in Go 1.21.

Abstract

The article provides insights into several advanced Go programming practices, particularly those related to context management and error handling. It begins by recapping the previous week's discussion on ignoring values, filtering with zero allocation, and converting if-else statements to switch cases. The current week's exploration includes the use of context.WithoutCancel() to prevent child context cancellations, the use of loop labels for cleaner control flow in nested loops, and the introduction of context.AfterFunc() for scheduling functions post-context cancellation. Additionally, the article advises against using panic() in production, advocating for robust error handling to prevent system crashes and ensure resilient applications.

Opinions

  • The author suggests that certain operations should continue even when their parent context is cancelled, which is facilitated by context.WithoutCancel().
  • Loop labels are recommended for complex nested loops to improve code readability and maintainability.
  • The new context.AfterFunc() feature is praised for its utility in executing cleanup or logging tasks after a context has been cancelled.
  • The article strongly advises against using panic() in production code, emphasizing that even with recover(), it can lead to program crashes and is not suitable for distributed systems.
  • The author believes that handling errors properly is crucial for building resilient systems, allowing for retries, default values, or informed decision-making in the face of failures.

Go EP6: Keep contexts going with context.WithoutCancel()

Everything from how to better control context to how to handle errors in a way that makes our apps work better and last longer.

Photo by Fitsum Admasu on Unsplash

First, let’s go over what we talked about last week:

  • 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.

This week, we’ll be going even deeper and looking at some subtle techniques and habits that will help us get better at writing code in this language.

The tips in this article cover everything from how to better control context to how to handle errors in a way that makes our apps work better and resilient:

  • Take 1: Keep contexts going with context.WithoutCancel()
  • Take 2: Loop labels for cleaner breaks and continues
  • Take 3: Scheduling functions after context cancellation with context.AfterFunc
  • Take 4: Just… don’t panic()

Take 1: Keep contexts going with context.WithoutCancel()

We already know that when a parent context is cancelled, all of its children are cancelled, right?

But sometimes, that’s not what we want.

There are scenarios where we need certain operations to proceed without being interrupted by the cancellation of their parent context.

Imagine you’re handling an HTTP request where, upon cancellation (client timeout, disconnection, etc.), you still want to log the request details and collect metrics.

“Hah, I’ll just need to create a new context for those operations”

That’s a solution, but the new context lacks the values from the original event’s context, which are important for tasks such as logging, collecting metrics.

Only a child context can be propagated with these values:

Now, back to our HTTP example, here is the solution:

WithoutCancel ensures that these operations can be completed without being halted by the request’s cancellation. By the way, this function was added in Go 1.21.

Take 2: Loop labels for cleaner breaks and continues

Labels and goto can be tricky and are often avoided because they can make code less readable:

The example above may seem clear due to its simplicity. But as the complexity grows, code can become hard to follow:

  • You might have to trace the label ‘hundreds of miles’ away from the goto statement.
  • With your eyes moving up and down the code, follow where the goto statement leads.

Loop labels

For instance, when managing nested loops, using loop labels is generally seen as good practice in certain contexts.

Imagine we’re searching for a number within 2D arrays:

Now, here’s a more elegant solution: a loop label allows you to name a loop.

Once named, you can use break orcontinue, followed by the label, to control not just the current loop but any surrounding loops as well. And the result?

Code that’s not only shorter but also cleaner and easier to understand.

We can use a loop label with both ‘break’ and ‘continue’.

Another useful case, when using a loop with select{}. If you use a break in a select statement without a label, you’ll only exit the select, not the loop containing it:

So, using a label in this case is necessary.

Take 3: Scheduling functions after context cancellation with context.AfterFunc

In take 1, we learned how to make a context continue even when its parent stops.

Now, let’s look at a new feature from Go 1.21, context.AfterFunc() lets you set up a callback function, f(), to run in its own goroutine after a ctx finishes (either because it’s canceled or it times out).

This feature is great for cleanup, logging, or other tasks after cancellation.

“When does the callback run?”

The callback runs in a new goroutine, which is triggered after receiving a signal from the ctx.done channel of the parent context.

“What if the context is already cancelled?”

The callback runs right away, of course, in a new goroutine.

Here are some key points:

  • Works on its own: You can use AfterFunc many times in the same context without any issues, each task you set up runs on its own.
  • Runs right away if context is done: If ctx is finished when you call AfterFunc, it starts f() right away in a new goroutine.
  • You can cancel the planned function: It gives you a stop function that can stop f from running.
  • Non-blocking: Using stop doesn’t wait for f to finish, it stops quickly. If you need f() and your main work to be in sync, you have to arrange that yourself.

Now, let’s talk a bit about stop(), the return of AfterFunc:

If we call stop() before the context finishes and the callback hasn’t run yet (actually, the goroutine hasn’t been triggered), stopped will be true.

This means we stopped the callback from running successfully. If stop() returns false, it can mean either:

  • The function f has already started running in a new goroutine.
  • The function f has already been stopped.

Take 4: Just… Don’t Panic()

Don’t panic() might seem like aggressive advice, but it is actually good practice for production.

“Why? Even though I can catch a panic with recover()?”

You may not be able to recover from a panic(), even with recover().

Here’s what I mean:

In the snippet above, the panic happens in a new goroutine (go panicFunc).

The important detail is that panic recovery with recover() only works if the panic occurs in the same goroutine as the recover() call.

As a result, the defer function in main cannot catch or recover, leading to a program crash despite the recovery attempt.

But that’s not the only reason, here are 2 others:

1. In production, your code needs to be robust.

Crashing unexpectedly is a big no-no because it can lead to downtime, which affects users and possibly your business’s reputation.

2. A panic in one part of a system can trigger a domino effect.

Leading to more failures throughout the system (cascading failure?), especially in microservices or distributed systems.

Let’s take a typical example:

This is not bad (?), but it encourages the use of panic. Instead, this should be better:

When an error is returned, instead of panicking, your program can make decisions on how to proceed, maybe:

  • Retry the operation
  • Use a default value
  • Log a detailed message for debugging.
  • Fatal

This flexibility is crucial for building resilient systems.

Panics should be the last resort

Only use panics for truly unrecoverable errors, this means situations where continuing to run the program could cause worse issues, like corrupt data or undefined behavior.

During program initialization, a panic might be “acceptable” if a critical component fails to start, as it indicates the program cannot run as intended.

Golang
Go
Programming
Coding
Software Development
Recommended from ReadMedium