The context package in go can come in handy while interacting with APIs and slow processes, especially in production-grade systems that serve web requests. Where, you might want to notify all the goroutines to stop work and return. Here is a basic
tutorial on how you can use it in your projects with some best practices and gotchas.
For understanding the context package there are 2 concepts that you should be familiar with.
I will try to briefly cover these before moving on to context, if you are already familiar with these, you can move on directly to the Context section.
Goroutine
From the official go documentation: “A goroutine is a lightweight thread of execution”. Goroutines are lighter than a thread so managing them is comparatively less resource intensive.
//function to print hello func printHello() { fmt.Println("Hello from printHello") }
func main() { //inline goroutine. Define a function inline and then call it. go func(){fmt.Println("Hello inline")}() //call a function as goroutine go printHello() fmt.Println("Hello from main") }
If you run the above program, you may only see it print out
Hello from main
that is because it fires up couple goroutines and the
main
function exits before any of them complete. To make sure
main
waits for
the goroutines to complete, you will need some way for the goroutines to tell it that they are done executing, that’s where channels can help us.
Channels
These are the communication channels between goroutines. Channels are used when you want to pass in results or errors or any other kind of information from one goroutine to another. Channels have types, there can be a channel of type
int
to receive integers or
error
to receive errors, etc.
Say there is a channel
ch
of type
int
If you want to send something to a channel, the syntax is
ch <- 1
if you want to receive something from the channel it will be
var := <- ch
. This
recieves from the channel and stores the value in
var
.
The following program illustrates the use of channels to make sure the goroutines complete and return a value from them to main.
Note: Wait groups (
https://golang.org/pkg/sync/#WaitGroup
) can also be used for synchronization, but since we discuss channels later on in the context section, I am picking them in my code samples for this blog post
//prints to stdout and puts an int on channel func printHello(ch chan int) { fmt.Println("Hello from printHello") //send a value on channel ch <- 2 }
func main() { //make a channel. You need to use the make function to create channels. //channels can also be buffered where you can specify size. eg: ch := make(chan int, 2) //that is out of the scope of this post. ch := make(chan int) //inline goroutine. Define a function and then call it. //write on a channel when done go func(){ fmt.Println("Hello inline") //send a value on channel ch <- 1 }() //call a function as goroutine go printHello(ch) fmt.Println("Hello from main")
//get first value from channel. //and assign to a variable to use this value later //here that is to print it i := <- ch fmt.Println("Recieved ",i) //get the second value from channel //do not assign it to a variable because we dont want to use that <- ch }
A way to think about context package in go is that it allows you to pass in a “context” to your program. Context like a timeout or deadline or a channel to indicate stop working and return. For instance, if you are doing a web request or running
a system command, it is usually a good idea to have a timeout for production-grade systems. Because, if an API you depend on is running slow, you would not want to back up requests on your system, because, it may end up increasing the load
and degrading the performance of all the requests you serve. Resulting in a cascading effect. This is where a timeout or deadline context can come in handy.
Creating context
The context package allows creating and deriving context in following ways:
context.Background() ctx Context
This function returns an empty context. This should be only used at a high level (in main or the top level request handler). This can be used to derive other contexts that we discuss later.
ctx, cancel := context.Background()
context.TODO() ctx Context
This function also creates an empty context. This should also be only used at a high level or when you are not sure what context to use or if the function has not been updated to receive a context. Which means you (or the maintainer) plans to
add context to the function in future.
ctx, cancel := context.TODO()
Interestingly, looking at the code (
golang.org/src/context…
), it is exactly same as background. The difference is,
this can be used by static analysis tools to validate if the context is being passed around properly, which is an important detail, as the static analysis tools can help surface potential bugs early on, and can be hooked up in a CI/CD pipeline.
var ( background = new(emptyCtx) todo = new(emptyCtx) )复制代码
context.WithValue(parent Context, key, val interface{}) (ctx Context, cancel CancelFunc)
This function takes in a context and returns a derived context where value
val
is associated with
key
and flows through the context tree with the context. This means that once you get a context with value, any context
that derives from this gets this value. It is not recommended to pass in critical parameters using context value, instead, functions should accept those values in the signature making it explicit.
This is where it starts to get a little interesting. This function creates a new context derived from the parent context that is passed in. The parent can be a background context or a context that was passed into the function.
This returns a derived context and the cancel function. Only the function that creates this should call the cancel function to cancel this context. You can pass around the cancel function if you wanted to, but, that is highly not recommended.
This can lead to the invoker of cancel not realizing what the downstream impact of canceling the context may be. There may be other contexts that are derived from this which may cause the program to behave in an unexpected fashion. In short,
NEVER
pass around the cancel function.
context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)
This function returns a derived context from its parent that gets cancelled when the deadline exceeds or cancel function is called. For example, you can create a context that will automatically get canceled at a certain time in future and pass
that around in child functions. When that context gets canceled because of deadline running out, all the functions that got the context get notified to stop work and return.
This function is similar to
context.WithDeadline
. The difference is that it takes in time duration as an input instead of the time object. This function returns a derived context that gets canceled if the cancel function is called
or the timeout duration is exceeded.
Now that we know how to create the contexts (Background and TODO) and how to derive contexts (WithValue, WithCancel, Deadline, and Timeout), let’s discuss how to use them.
In the following example, you can see a function accepting context starts a goroutine and waits for that goroutine to return or that context to cancel. The select statement helps us to pick whatever case happens first and return.
<-ctx.Done()
once the Done channel is closed, the
case <-ctx.Done():
is selected. Once this happens, the function should abandon work and prepare to return. That means you should close any open pipes, free resources
and return form the function. There are cases when freeing up resources can hold up the return, like doing some clean up that hangs, etc. You should look out for any such possibilities while handling the context return.