Background
As a Golang user and learner, I always think Golang standard package is a great learning resource, which can provide best practices for both the language itself and various software or programming concepts.
In this post, I will share what I learned about package context
.
context
is widely used in the Golang ecosystem, and I bet you must often come across it. Many standard packages rely on it.
There are many good articles online explaining the background and usage examples of context
, I will not spend too much time on that, just add a brief introduction here.
The problems context
plans to solve are:
- Let’s say that you started a function and you need to pass some common parameters to the downstream functions. You cannot pass these common parameters each as an argument to all the downstream functions.
- You started a goroutine which in turn start more goroutines and so on. Suppose the task that you were doing is no longer needed. Then how to inform all child goroutines to gracefully exit so that resources can be freed up
- A task should be finished within a specified timeout of say 2 seconds. If not it should gracefully exit or return.
- A task should be finished within a deadline eg it should end before 5 pm . If not finished then it should gracefully exit and return
You can refer to this slide from the author of context package to understand more about the background.
In this post, I will show you the details of context package source code. You can find all the related source code inside the context.go
file. You will notice that context
package content is not long, and there are roughly 500 lines of code. Moreover, there are many comments, so the actual code is only half. These 200+ lines of code are a great piece of learning resource in my eyes.
Source code analysis
Context interface and emptyCtx
The most basic data structure of context is the Context
interface as below:
1 | type Context interface { |
Context
is just an interface, which is very hard to imagine how to use it. So let us continue reviewing some types implement such interface.
When context is used, generally speaking, the first step is creating the root context with context.Background()
function(the contexts are chained together one by one and form a tree structure, and the root context is the first one in the chain). Let’s check what it is:
1 | var background = new(emptyCtx) |
Background
function return the background
which is a global variable declared as new(emptyCtx)
. So what is emptyCtx
, let continue:
1 | // An emptyCtx is never canceled, has no values, and has no deadline. It is not |
You can see that emptyCtx
is declared as a new customized type based on int
. In fact, it’s not important that emptyCtx
is based on int
, string
or whatever. The important thing is all the four methods defined in interface Context
return nil
. So the root context is never canceled, has no values, and has no deadline.
Let’s continue to review other data types.
valueCtx and WithValue
As mentioned above, one typical usage of context is passing data. In this case, you need to create a valueCtx
with WithValue
function. For example, the following example:
1 | rootCtx := context.Background() |
WithValue
is a function has only one return value:
1 | func WithValue(parent Context, key, val interface{}) Context { |
Please ignore the reflectlite
part, I will give a in-depth discussion about it in another post. In this post, we only need to care the return value type is &valueCtx
:
1 | type valueCtx struct { |
There is one interesting Golang language feature here: embedding
, which realizes composition
. In this case, valueCtx
has all the four methods defined in Context
.
In fact, embedding
is worthy much more discussion. Simplying speaking, there are 3 types of embedding: struct in struct, interface in interface and interface in struct. valueCtx
is the last type, you can refer to this great post
When you want to get the value out, you can use the Value
method:
1 | func (c *valueCtx) Value(key interface{}) interface{} { |
If the provided key
parameter does not match the current context’s key, then the parent context’s Value
method will be called. If we still can’t find the key, the parent context’s will call its parent as well. The search will pass along the chain until the root node which will return nil
as we mentioned above:
1 | func (*emptyCtx) Value(key interface{}) interface{} { |
Next, let’s review another interesting type: cancelCtx
cancelCtx and WithCancel
First, let’s see how to use cancelCtx
and WithCanel
with a simple example:
1 | package main |
When main goroutine wants to cancel task
goroutine, it can just call cancelFunc
. Then the task goroutine will exit and stop running. In this way, goroutine management will be easy task. Let’s review the code:
1 | type CancelFunc func() |
cancelCtx
is complex, let’s go through bit by bit.
WithCancel
returns two values, the first one &c
is type cancelCtx
which is created with newCancelCtx
, the second one func() { c.cancel(true, Canceled) }
is type CancenlFunc
(just a general function).
Let’s review cancelCtx
firstly:
1 | func newCancelCtx(parent Context) cancelCtx { |
Context
is embedded inside cancelCtx
as well. Also it defines several other fields. Let’s see how it works by checking the receiver methods:
1 | func (c *cancelCtx) Done() <-chan struct{} { |
Done
method returns channel done
. In the above demo, task goroutine listen for cancel signal from this done channel like this:
1 | select { |
The signal is trigger by calling the cancle function, so let’s review what happens inside it and how the signals are sent to the channel. All the logic is inside cancel
method of cancelCtx
:
1 | func (c *cancelCtx) cancel(removeFromParent bool, err error) { |
As shown above, cancelCtx
has four properties, we can understand their purpose clearly in this cancel
:
mu
: a general lock to make sure goroutine safe and avoid race condition;err
: a flag representing whether the cancelCtx is cancelled or not. When the cancelCtx is created,err
value isnil
. Whencancel
is called for the first time, it will be set byc.err = err
;done
: a channel which sends cancel signal. To realize this, context justclose
the done channel instead of send data into it. This is an interesting point which is different from my initial imagination before I review the source code. Yes, after a channel is closed, the receiver can still getzero value
from the closed channel based on the channel type. Context just make use of this feature.children
: aMap
containing all its child contexts. When current context is cancelled, the cancel action will be propogated to the children by callingchild.cancel(false, err)
in the for loop. Then next question is when the parent-child relationship is established? The secret is inside thepropagateCancel()
function;
1 | func propagateCancel(parent Context, child canceler) { |
propagateCancel
contains many logics, and some of them can’t be understood easily, I will write another post for those parts. But in this post, we only need to understand how to establish the relationship between parent and child for genernal cases.
The key point is function parentCancelCtx
, which is used to find the innermost cancellable ancestor context:
1 | func parentCancelCtx(parent Context) (*cancelCtx, bool) { |
You can notice that Value
method is called, since we analyzed in the above section, Value
will pass the search until the root context. Great.
Back to the propagateCancel
function, if cancellable ancestor context is found, then current context is added into the children
hash map as below:
1 | if p.children == nil { |
The relationship is established.
Summary
In this article, we review the source code of Context
package and understand how Context
, valueCtx
and cancelCtx
works.
Context
contains the other two types of context: timeOut
context and deadLine
context, Let’s work on that in the second part of this post series.