How to work with Golang contexts and blocking functions

How to work with Golang contexts and blocking functions

Once you’ve been programming long enough, you’re bound to encounter issues where an application would become stuck intermittently, but for no obvious reason. With the root cause found and the issue resolved, you might ask yourself “how can I keep this kind of bug from happening in the future?”

In this post I suggest a possible method for preventing this kind of issue with the help of the compiler: in function signatures, you would indicate that they can block by having a Context argument and allowing the caller to take the necessary precautions to avoid blocking for too long (or at all).

Similarity to the convention of errors as return values

In Go, whenever you call a function that returns an error, you must check for an error — or risk the function having not done what it was supposed to do. If you handle the error, then all’s fine and well: you need not propagate it to your callers. If you don’t handle it then, by convention, you simply propagate it up by returning the error.

For example, os.Getenv - func Getenv(key string) string cannot fail: it either returns the value of the environment variable, or an empty string if the environment variable did not exist. On the other hand, http.Get - func Get(url string) (resp *Response, err error) can fail: if it fails, you should handle the failure or tell your caller by returning an error yourself.

This means that, when you write a function that calls other functions that can return errors, you are forced to explicitly make this choice: handle the error or propagate it to your callers.

With IO or blocking operations, a similar complexity emerges: you could call a function and not know how it would behave, absent documentation. Can it block? For how long? If it can block, how do you set a timeout? How do you cancel an ongoing operation? You can only answer these questions through the documentation or reading the code. If you rely the documentation, it might not be up to date: some subtle property could have changed since the documentation was written and which makes the function potentially blocking, but you won’t know that as the function signature itself tells you nothing about this.

Golang Contexts

You can use contexts, as in Context from the context package, to surface the complexity of your function performing some potentially blocking operation. They can also allow it to be canceled and to specify a timeout, forcing the caller to handle the possibility of your function taking a variable and unknown amount of time to return.

When is a Context useful?

Imagine you were asked to implement a mechanism that reports logged errors to an external error tracking service, such as Bugsnag or Sentry, but the requirement is that it only report errors from production.

Your codebase has a configuration package that uses environment variables to determine the current configuration. You decide to add a function that tells you whether errors should be reported:

func ShouldReportErrors() bool {
  reportBugs := os.Getenv("SHOULD_REPORT_ERRORS")
  if reportBugs == "true" {
    return true
  }

  return false
}

As it is, this function can never block or fail — so it does not return an error (cannot fail), and it also does not take a context.Context parameter (cannot block).