Simplifying context in Go
How can you simplify the Context package in Go, and what is its purpose? In this article, I'll guide you step by step and explain how to use the Context package.
When I created my first goroutine in Go, I stumbled upon a question. What if this goroutine has a bug and never finishes? The rest of my program might keep on running oblivious of a goroutine that never finishes. The most simplest example of this situation is a simple “throwaway” goroutine that runs an infinite loop.
The above example is not complete but I hope you understand what I'm trying to do. Our “throwaway” goroutine might or might not process data successfully. It might enter an infinite loop or cause an error. The rest of our code would not know what happened.
There are multiple ways to solve this problem. One of them is to use a channel to send a signal to our main thread that this goroutine is taking to long and that it should be cancelled.
Pretty straightforward. We are using a channel to signal to our main thread that this goroutine is taking too long. But the same thing can be done with context and that is exactly why the context package exists.
If you concluded that the context package is a wrapper around a channel to which you react to, you would be right. You could easily recreate the context package on your own, but context package is part of the Go SDK and it is preferable to use it. Also, this package is used by many libraries out there including the http package, among others.
Every time you create a new context you get a type that conforms to this interface. Real implementation of context are hidden in this package and behind this interface. These are the factory types of contexts that you can create:
The first context types that we will look at are the context.TODO and context.Background
context.TODO and context.Background
These types of context basically do nothing. It's as if you haven't even created them. Their only use is that you pass some function a context but you don't plan to do anything with that context. Basically a throwaway context. Use this context when you don't plan do anything with it but some API that you are using requires that you pass it a context.
These types are also used when you need to create a context based on another context, making it a parent context. We will see the examples for that later on.
It is also very important to say that, if a function requires you to pass a context, it is for a good reason. This could be an HTTP request, a connection to a database or a query to the database. HTTP can hang and database queries could take some time. It is good practice that you pass a context in these situations since they protect you from breaking your program. For example:
Question from this piece of code is, how long will this request take? Sure, we're calling Google but even Google is not immune from some downtime. A better solution is to create a context that will tell us when this request is taking too long (or at least what we think “too long” is) so we can react to it. A better solution is to implement a timeout context and react when that timeout exceeds. We will go back to this example when we talk about some other types of contexts.
Back to context.TODO(). Under the hood, this context is of type *emptyCtx and it basically returns empty values for every function in the Context interface. But this context uses a different purpose. It serves as the parent context for some more useful context types. context.Background() is equal to context.TODO. When researching for this blog post, the only difference is semantics. With context.Background(), you are signaling to other developers that you should do something with this context, but from the context package source code, the are the same. If you disagree, leave a comment.
Let's dive into some more interesting uses of the context package.
Parent context and canceling
context.TODO and context.Background are types that are used as parent context for other, more useful types of contexts. The first one we will take a look at is the the cancel context and its uses.
Let's say you create a goroutine worker that does something very expensive for your CPU. If the work that it is doing is no longer required, you would like it to stop so it does not waste any resources. Normally, you would do this with regular channels.
The code is pretty straightforward. We are using a channel to signal to the goroutine to stop working after 2 seconds. Now let's try this with a canceller context.
From the documentation:
Calling the CancelFunc cancels the child and its children, removes the parent's reference to the child, and stops any associated timers. Failing to call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires.
So, when we called cancel(), select case for ctx.Done() was fulfilled and we could return from the goroutine.
A very important thing to say here is that you always defer cancel(). From the official Go blog:
Always defer a call to the cancel function that’s returned when you create a new Context with a timeout or deadline. This releases resources held by the new Context when the containing function exits
Parent <-> Child relationship
Contexts work based on a parent child relationship. When you create a context from another context, that created context is said to be derived from the parent context. If you cancel the parent context, all of its children are cancelled as well. You can create as many derived contexts as you like. Here, we are creating 2 derived contexts and cancelling the parent context. After the parent is cancelled, the child is cancelled as well.
As you can see, the first one to be cancelled are the children. The last one to be cancelled is the parent.
Timers -> context.WithTimeout and context.WithDeadline
WithTimeout and WithDeadline and contexts that are set to automatically cancel when some time expires or reaches, depending on the type. They are essentially the same, but WithDeadline receives time.Time while WithTimeout accepts time.Duration but returns a WithDeadline context. From the source code:
WithDeadline can be set to expire in some future date and time. For example, the code below takes whatever date and time you are reading this and expires in 2 seconds in the future.
As I said, WithTimeout is the same as WithDeadline, but it only accepts a time.Duraction time. The code above could be written in the same way WithTimout:
Context package is great for concurrency patterns and essential to learn well since most of the libraries that are dealing with some kind of timers or future resolutions use context. This includes database connections, HTTP requests etc…
If you would like to know more about this package here are a few useful links:
Context package official documentation
Official Go blog post about concurrency and context
This blogging platform is created specifically for software developers. We aim to support many more programming languages and development environments but for that, we need your support. If you like this blogging platform, consider using it to write your blogs.
We tried to make your experience of creating blog as painless as possible soSign in and give it a try.