cft

How to use the Context Package in Go

The context package in Go can be used to provide cancellation signals and request details. It is a widely-used tool that has many use cases, such as performing a graceful shutdown of long running tasks


user

Leonardo Rodrigues Martins

15 days ago | 6 min read

Photo by Fotis Fotopoulos on Unsplash

As application size grows, it becomes more important to keep track of active processes and properly handle them when they are no longer needed. Otherwise, it can lead to wasting computing time and resources.

Let’s consider a request coming into a web server, as soon as it is received, the server starts performing its corresponding tasks. However, in the case the client gets disconnected, the server would continue handling the request even though its response is no longer needed. The context package can help us to identify when such processes are no longer necessary and can be stopped.

In this post, we will go through how to create and use the context package to improve the reliability of a Golang application.

Context

The context package in go is a built-in library, it has defined within the Context interface. The method Done is particularly important since it allows us to identify when a context terminates:

type Context interface {

...

⁠ Done() <-chan struct{}

// Other methods below

...

}

Consider the following goroutine:

func GetValue(ctx context.Context, output chan int) error {

for {

v, err := GetInt(ctx)

if err != nil {

return err

}

select {

case <-ctx.Done():

return nil

case output <- v:

}

}

}

The for-select structure will continue to send values to the output channel until the Done method channel unblocks.

As a convention in GO, the context is set as the first parameter of a function. In this situation, the context is used to stop an operation that runs as a goroutine. Usually, whenever you have an API call in your code, you have to include a context to it as well.

Context Creation

There are different ways of creating a context. It can be done whenever you start routines that need to have their termination controlled. Also, goroutines can even have child routines controlled by deriving other contexts from a parent one.

TODO Context

The TODO is an empty dummy context that can be employed when it is not clear yet how a specific request or routine must be handled. Its creation goes as follows:

ctx := context.TODO()

Background Context

There is another kind of basic context that can be used, which is referred to as Background. It is also an empty context, its creation uses the context.Background function from the package.

ctx := context.Background()

Background vs TODO

At first, both contexts seem to be the same. As it turns out, they are, but they serve to demonstrate different purposes in your code.

  • TODO context signals that there is still work to be done. In other words, it can be used as a temporary input for a determined API request or goroutine.
  • Background context makes clear that you intend to create an empty context. Normally it serves as a root context, from which other ones can be derived.

Since both are empty, they are not able to do much. Let's see how we can derive some more sophisticated contexts from them.

Passing Values to the Context

Contexts in GO have the ability to carry around values within that can be identified by a certain key (just as a map works)

func task(ctxWithTimeout context.Context) {

// New Context creation.

ctx := context.WithValue(ctxWithTimeout , "id", 0)

// Retrieving the id value

id:= ctx.Value("id")

...

}

Values inside contexts are read-only variables, it can be used to share values among several goroutines.

Although contexts are able to carry any kind of value in it, you should use them cautiously. As if you include too many variables within, it can lead to an unclear and much harder-to-maintain code. Generally, if you need to hold a variable that is going to be used in one place only, it might be a better option to pass it as a normal parameter.

Terminating the Context

The empty Background or TODO context does not have any specification regarding termination. Fortunately, there are different ways of creating a context that has determined exactly when it should stop.

Ending with a Cancel Function

The cancel function provides a direct way of controlling the cancellation of a context:

ctx, cancel := context.WithCancel(ctx)

outputInts := make(chan int)

go GetValue(ctx, outputInts)

for i := 1; i <= 3; i++ {

print(<-outputInts)

}

cancel()

The diagram below illustrate what happens in the snippet above:

Where V1, V2, V3 stand for values 1, 2 and 3.

The WithCancel function gives the possibility to control the exact moment a context ends. In the example above, the GetValue routine sends three values to the output channel, which are going to be read and printed. After the third iteration of the for-loop, the cancel function gets called.

In the for-select structure, after the cancel function is called, the first alternative of the select statement will be enabled, which ends the goroutine right away.

It is important to outline that the parent context (the one used as input for the WithCancel function) will not end as the cancel function is called, it will remain intact. In contrast, when the parent context terminates, all its children contexts will end as well.

Ending with a Deadline

There is the possibility to end a context by determining a deadline by which it should terminate. For example:

A given task started to run today at 3 pm, with the deadline context, we can determine that it should finish by 3:10.

After we reach the deadline, the context will automatically send the termination signal to the tasks depending upon it.

The deadline context can be created using the WithDeadline function, which takes as input the parent context and a Golang Time value saying when it should cancel. Just like in the cancel function case, when the deadline exceeds the parent context will not be affected.

cancelDeadline := time.Now().Add(10 * time.Second)

ctx, cancel := context.WithDeadline(ctx, cancelDeadline)

defer cancel()

outputInts := make(chan int)

go GetValue(ctx, outputInts)

for i := 1; i <= 3; i++ {

print(<-outputInts)

}

In this case, we have specified the context deadline with the time package in Go, which is another standard library available. The WithDeadline function also returns a cancellation function, which enables us to cancel the context before reaching the deadline.

In the example above, taking the instant t=0 as the start, we have set the deadline to the instant t=10

Ending with a Timeout

Lastly, there is also the option to end a context by passing a timeout value at the moment of the context creation. It works pretty similarly to the deadline case, the difference is that we specify a direct time duration for the context. In the case of the WithDeadline function, we had to give the exact timestamp the context was supposed to terminate. In the timeout situation, we specify how long it is supposed to last.

Normally, this is going to be a more useful option compared to the previous one. However, with both available, you can choose which suits you best.

ctx, cancel := context.WithTimeout(ctx, 10 * time.Second)

defer cancel()

outputInts := make(chan int)

go GetValue(ctx, outputInts)

for i := 1; i <= 3; i++ {

print(<-outputInts)

}

Thinking of a request to a server, for instance, you can set that a request should take no more than 5 seconds. The WithTimeout function takes the responsibility of figuring out the exact moment the context must end.

These are some of the ways we can use to optimize resource usage in a Golang application. It can be useful to avoid memory leak problems which may be harder to figure out as the application grows.

In addition, in cases where you are running a server, you might need to perform some cleanup tasks before finishing the application.

Best Practices

Also, here are some of the best practices we have when dealing with the context package in go.

  • Always call the cancel function with a defer statement as well. It will take care of the cancellation of the context despite how many return statements have been added
  • The TODO context should be used as a temporary context input. Replace it as soon as it gets clearer which context should be used there.
  • Make sure to include Timeout/Deadlines to the tasks that may potentially be left hanging around for a long time without needing.
  • Use the background context at the root of your application and derive other contexts from it.
  • Just because you can store anything in a context doesn’t mean you should. Consider if it is really necessary, as it can make your code harder to understand.

Conclusion

In this tutorial, we have explored some of the options the context package gives us to handle some common issues in Go while managing multiple goroutines. The package provides several option to create and handle contexts in a flexible way.

Contexts give the possibility of determining when a task should end and performing any clean-up job if needed, the WithTimeout, WithDeadline, and WithCancel functions give many possibilities.

Overall, it is another tool that can be used to develop a more robust and effective code. Thanks for making it to the end, I hope this tutorial has been useful to you!

Upvote


user
Created by

Leonardo Rodrigues Martins

Devops Engineer

I'm currently working as a DevOps Engineer. I have also experience as a Software Developer. I enjoy sharing my insights and tips to other developers and writing about them.


people
Post

Upvote

Downvote

Comment

Bookmark

Share


Related Articles