Golang: context from the inside
Introduction
It's no secret that the standard context package is widely used to create your services. At the moment, I'm not afraid to say it, any service written in Go uses contexts everywhere. My opinion is that if you want to progress as a specialist, you should dig deeper and deeper. I suggest considering context from the prism of its work inside.
Definition of Context
There are several types of context that a Golang developer will encounter. Let's go over them briefly and then delve into the essence of each. But first, let's pay attention to the context creation functions.
context.Background() Context
– used to create the root context and cannot be canceledTODO() Context
– used as a placeholder if you haven't yet defined what context you need and you will redefine itWithCancel(parent Context) (ctx Context, cancel CancelFunc)
– creates a child context with a cancel method from the parent context that can be called manuallyWithDeadline(parent Context, d time.Time) (Context, CancelFunc)
– creates a child context using the cancel method from the parent context, except that the context will be automatically canceled after the specified time has been reachedcontext.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
– the same asWithDeadline
except that it specifies the timeout from the current timeWithValue(parent Context, key, val any) Context
– creates a child context from the parent context that can store a key-value pair and is a context and also cannot be canceled
The definition of Context itself is in the context package and is an interface.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
And you can read the description of this interface
A Context carries a deadline, a cancellation signal, and other values across API boundaries. Context's methods may be called by multiple goroutines simultaneously.
It can also be noted that incoming requests to a server must create a context, and outgoing calls to servers must accept it. The chain of function calls between them must propagate the context, replacing it with a derived context created by WithCancel
, WithDeadline
, withTimeout
or withValue
When a context is canceled, all contexts derived from it are also canceled.
Which means we have to accept context and then redefine it further down the call chain, because context.Background()
should essentially be the initial context, where branches follow.
You can see the tip hidden in the package description.
Do not store Contexts inside a struct type; Instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
func DoSomething(ctx context.Context, arg Arg) error {
… use ctx …
}
And if you translate it literally, it means that you don’t need to store contexts inside the structure, it’s better to pass them to the function that needs it, you can also notice the note that follows pass it as the first parameter and usually call it ctx. This error is usually encountered by beginners who are just entering the world of Golang, which greatly affects the readability of the code.
Well, we can highlight a very simple piece of advice that you need to pass context only in the request data area, and not in a function or method as an optional parameter.
Deadline
The Deadline method itself returns the time when the task was executed on behalf of the current context. And if the deadline is not set, it returns the ok value, which is false. With successive calls to the Deadline function, the results will be the same. What is useful to know: how different types of contexts implement this method, we will look at a little later.
Done
Let's take a closer look at this method.
When work being performed on behalf of a context should be canceled, Done returns a channel that is closed.
If this context can never be canceled, Done returns nil
Closing the channel
Done
may occur asynchronously after the cancel function returnsSuccessive calls to Done return the same value
And what's most convenient is that we can use Done in select.
func StreamWithDone(ctx context.Context, out chan<- Value) error {
for {
value, err := Process(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- value:
}
}
}
Err
It's no more difficult to deal with than with Done.
If the parameter
Done
not closed yet,Err
returns nilIf the parameter
Done
closed,Err
returns a non-zero error explaining why canceled if the context was canceled or canceled on expiration if the context expiredAfter
Err
returns a non-zero error, subsequent callsErr
return the same error
Value
This is where things get more interesting.
Returns the value associated with this context for key, or nil if no value is associated with key.
Sequential calls
Value
using the same key returns the same resultThe key can be of any type that supports equality – packages should define keys as a non-exported type to avoid collisions
A key identifies a specific value in a context.
Packages that define a context key must provide type-safe accessors for values stored using that key.
Let's analyze the last statement. It is a good practice to create a separate key type and access the value to the context by it, and the key should be non-exportable, as this is necessary to avoid key conflicts with other packages, for example.
// Создаем ключ для контекста
type key int
// Инициализация переменной
var someKey key
// Создаем контекст
func NewContext(ctx context.Context, u *SomeStruct) context.Context {
return context.WithValue(ctx, someKey, u)
}
// Возращаем значение
func FromContext(ctx context.Context) (*SomeStruct, bool) {
someStruct, ok := ctx.Value(someKey).(*User)
return someStruct, ok
}
Important note! Passing values by context is a bad practice. Pass values by parameters, use passing by context only in forced situations. For example, you can use passing by logger context, some middleware ID, but again, all decisions should be thought out. There are several reasons for this, but one of them is that with a large nesting, the enumeration of contexts suffers, documentation also worsens, a problem of synchronization of the team's work is created (since if we use the context as a storage, then who knows, suddenly the data will be accidentally lost along the way unnoticed and that's it 🙂 )
You can disassemble the function Cause()
:
func Cause(c Context) error {
if cc, ok := c.Value(&cancelCtxKey).(*cancelCtx); ok {
cc.mu.Lock()
defer cc.mu.Unlock()
return cc.cause
}
return c.Err()
}
Cause
returns a non-zero error that explains why the context was canceled. The reason for the cancellation is the first time the context or one of its parents has been canceled. If the cancellation occurred with CancelCauseFunc(err)
That Cause
returns the error value. Otherwise, the value c.Err()
. If the context has not yet been canceled, Cause
returns nil.
Note: We know that a context is not a descendant of any context created by WithCancelCause
because it has no meaning cancelCtxKey
. If it is not one of the standard context types, an error may occur, even without a reason..
We will consider the usage itself below in the article.
Internal context types
emptyCtx
emptyCtx
– is a structure that is never cancelled, returns nil for all its methods, has no values and no deadline, and implements the Context interface. It is used to create a root context that returns context.Background()
And context.TODO()
in the standard library.
type emptyCtx struct{}
func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (emptyCtx) Done() <-chan struct{} {
return nil
}
func (emptyCtx) Err() error {
return nil
}
func (emptyCtx) Value(key any) any {
return nil
}
type backgroundCtx struct{ emptyCtx }
func (backgroundCtx) String() string {
return "context.Background"
}
type todoCtx struct{ emptyCtx }
func (todoCtx) String() string {
return "context.TODO"
}
We can also see the definition of methods String()
For todoCtx
And backgroundCtx
which returns a named representation of TODO and Background.
cancelCtx
This is a context that can be canceled, and when canceled, all its children that implement the cancel function are canceled. It is created by WithCancel()
.
type cancelCtx struct {
Context // родительский контекст
mu sync.Mutex // защищает следующие поля
done atomic.Value // из канала struct{}, созданной лениво, закрытым первым вызовом cancel
children map[canceler]struct{} // устанавливается равным nil при первом отмене вызова
err error // устанавливается на отличное от nil значение при первом вызове отмены
cause error // устанавливается на отличное от nil значение при первом вызове отмены
}
cancelCtx
is defined as a context that can be canceled. Because of the tree structure of the context, when canceling, all child contexts must be canceled synchronously. You just need to traverse the structure children map[canceler]structure{}
and cancel them one by one.
What is it in essence? canceler
? It's just an interface with certain fields. Its implementations are *timerCtx
And *cancelCtx
type canceler interface {
cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{}
}
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
// Просто лочим мьютекс и просто возращаем саму ошибку в струкутуре контекста
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
type stringer interface {
String() string
}
func contextName(c Context) string {
if s, ok := c.(stringer); ok {
return s.String()
}
return reflectlite.TypeOf(c).String()
}
func (c *cancelCtx) String() string {
return contextName(c.Context) + ".WithCancel"
}
We see a normal definition of the Context interface, where everything is simply synchronized with mutexes inside for safe access to the channel. But the next method is more interesting.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
// назначаем родительский контекст для текущего контекста отмены
c.Context = parent
// если родительский контекст не поддерживает отмену (его метод Done возвращает nil), метод завершается
done := parent.Done()
if done == nil {
return // родитель никогда не отменяется
}
select {
case <-done:
// родитель уже отменен
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
// добавляем дочерний контекст в список children родительского контекста, чтобы он мог быть отменен вместе с родительским
if p, ok := parentCancelCtx(parent); ok {
// родитель это *cancelCtx, или является производным от него
p.mu.Lock()
if p.err != nil {
// родитель уже отменен
child.cancel(false, p.err, p.cause)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
return
}
if a, ok := parent.(afterFuncer); ok {
// родитель имплементирует AfterFunc метод
c.mu.Lock()
stop := a.AfterFunc(func() {
child.cancel(false, parent.Err(), Cause(parent))
})
c.Context = stopCtx{
Context: parent,
stop: stop,
}
c.mu.Unlock()
return
}
// если ни один из предыдущих условий не выполнен,
// метод запускает новую горутину, которая ожидает сигнала об отмене
// родительского контекста и, в случае его получения, отменяет дочерний контекст
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
propagateCancel
arranges for the cancellation of a child element in the presence of a parent, sets the parent context cancelCtx
. Thus, the method propagateCancel
ensures that the cancellation of a parent context is propagated correctly to all child contexts, ensuring consistency and simplifying lifetime management of related operations.
As can be seen from the source code of creation cancelCtx
internal alarm cancelCtx
depends on the channel Done
If you want to cancel this context, you need to block all <-c.Done ()
. The easiest way is to close this channel or replace it with an already closed channel..
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
// просто проверяется наличие ошибок
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // уже отменен
}
c.err = err
c.cause = cause
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
// отменяем все дочерние контексты
for child := range c.children {
child.cancel(false, err, cause)
}
// очищаем нашу мапу
c.children = nil
c.mu.Unlock()
// если removeFromParent == true, то удаляем текущий контекст из дочерних
if removeFromParent {
removeChild(c.Context, c)
}
}
In this method, we ensure that the current context and all its child contexts are correctly canceled, ensuring that all operations associated with them are properly completed.
You can look at the definition for greater clarity. removeChild()
func removeChild(parent Context, child canceler) {
// Проверяем, является ли родительский контекст типом stopCtx
if s, ok := parent.(stopCtx); ok {
s.stop() // Останавливаем родительский контекст
return
}
// Проверяем, является ли родительский контекст типом cancelCtx
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
// если у родительского контекста есть дочерние контексты,
// удаляем текущий дочерний контекст
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
In general, the logic should be clear, the only thing is the definition stopCtx
We will discuss this below in the article.
WithCancel
This context is created with the help of WithCancel(parent Context) (ctx Context, cancel CancelFunc)
.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
WithCancel
returns a copy of the parent with a new channel Done
. Channel Done
context is closed when the cancel function is called or when the channel is closed Done
parent context. Cancelling this context frees up resources, so the code should call cancel
after completing operations in this context.
Also, cancellation must be called at the level where it was created. Calling it elsewhere is an anti-pattern, as it can lead to leaking unclosed contexts.
CancelFunc
is a normal function.
type CancelFunc func()
Function CancelFunc
informs the operation that it is finished, but does not wait for it to complete. It can be called by several subroutines at the same time. After the first call, subsequent calls do nothing.
We can pay attention to withCancel()
.
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}
There is nothing supernatural in it, everything that was discussed above is fulfilled here.
timerCtx
It is built on top of cancelCtx
. The only difference is adding a timer and a cutoff time. With these two configurations, you can automatically cancel the timer at a specific time using the methods Deadline
And WithTimeout
.
type timerCtx struct {
cancelCtx
timer *time.Timer // Под cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) String() string {
return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
c.deadline.String() + " [" +
time.Until(c.deadline).String() + "])"
}
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Удаляет этот timerCtx из дочерних элементов его родительского cancelCtx
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
That is, the definition here is quite simple and clear.
WithDeadline
To see how ours is created timerCtx
let's pay attention to WithDeadline
.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
Let's review the main points:
WithDeadline
returns a copy of the parent context with the modified timeout, which must be set no later thand
.If the parent context's deadline is already set to
d
,WithDeadline(parent, d)
semantically equivalentparent
.Returned channel
Context.Done
which is closed when the timer expires, when the function is calledcancel
or when the parent context channel terminatesclosed
whichever comes first.Cancelling this context releases the resources associated with it, so the code should cancel the call as soon as the operations performed in it are complete.
We are giving back WithDeadLineCause()
:
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
// Проверка наличия родительского контекста
if parent == nil {
panic("cannot create context from nil parent")
}
// Проверка текущего дедлайна родительского контекста
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// Текущий дедлайн уже раньше нового дедлайна
return WithCancel(parent)
}
// Создание нового контекста с таймером
c := &timerCtx{
deadline: d,
}
c.cancelCtx.propagateCancel(parent, c)
// Расчет времени до дедлайна и проверка его прошедшего
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, cause) // дедлайн уже прошел
return c, func() { c.cancel(false, Canceled, nil) }
}
// Настройка таймера для отмены контекста по истечении дедлайна
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, cause)
})
}
// Возвращение контекста и функции отмены
return c, func() { c.cancel(true, Canceled, nil) }
}
valueCtx
It contains an instance of the interface Context
which is a child context, and two fields key, val interface {}
. When calling valueCtx.Value(key interface ())
a recursive search is performed. It is only responsible for finding the context. There is no way to know whether a related context contains this key.
type valueCtx struct {
Context
key, val any
}
It has only 2 methods.
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(" +
stringify(c.key) + ", " +
stringify(c.val) + ")"
}
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
Well, and the definition itself value
.
func value(c Context, key any) any {
for {
// сперва определяем тип контекста
switch ctx := c.(type) {
// если контекст является типа valueCtx,
// проверяется, совпадает ли ключ key с ключом,
// хранящимся в этом контексте (ctx.key). Если да, то
// возвращается соответствующее значение ctx.val
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
// тут возращает сам контекст
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
// если контекст является типа withoutCancelCtx,
// проверяется ключ на совпадение с cancelCtxKey.
// Если да, возвращается nil, что указывает на то,
// что контекст создан без поддержки отмены (метод Cause(ctx) возвращает nil).
// Если нет, переход к следующему контексту
case withoutCancelCtx:
if key == &cancelCtxKey {
// Имплементирует Cause(ctx) == nil
// когда ctx создан с использованием WithoutCancel
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}
The method recursively walks the entire context chain and returns the value associated with the specified key if such a value is found. If the key is not found in any context, it returns nil
.
stopCtx
type stopCtx struct {
Context
stop func() bool
}
Used as a parent context for the stop function, which unregisters the AfterFunc function. It has no methods.
aferFuncCtx
type afterFuncCtx struct {
cancelCtx
once sync.Once // либо запускает f, либо останавливает запуск f
f func()
}
func (a *afterFuncCtx) cancel(removeFromParent bool, err, cause error) {
a.cancelCtx.cancel(false, err, cause)
if removeFromParent {
removeChild(a.Context, a)
}
a.once.Do(func() {
go a.f()
})
}
It has only one method and contains a cancellation context and a synchronization primitive. Once
which is performed only once.
AfterFunc
func AfterFunc(ctx Context, f func()) (stop func() bool) {
a := &afterFuncCtx{
f: f,
}
a.cancelCtx.propagateCancel(ctx, a)
return func() bool {
stopped := false
a.once.Do(func() {
stopped = true
})
if stopped {
a.cancel(true, Canceled, nil)
}
return stopped
}
}
Function AfterFunc
accepts a function that will be executed after the context has finished, including timeout cases. If the context has already finished, the function will run immediately. The function is executed in a separate thread. In this case, each call AfterFunc
is performed independently of others.
AfterFunc returns a stop function. When the stop function is called, the connection between the function and the context is broken. If the context is already in the state Done
and the function has already been started, or if the function has already been stopped, then the stop function returns false.
The function terminates if the value true. Function stop does not wait for the function to complete, so it is recommended to explicitly interact with it to control the state.
withoutCancelCtx
type withoutCancelCtx struct {
c Context
}
func (withoutCancelCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (withoutCancelCtx) Done() <-chan struct{} {
return nil
}
func (withoutCancelCtx) Err() error {
return nil
}
func (c withoutCancelCtx) Value(key any) any {
return value(c, key)
}
func (c withoutCancelCtx) String() string {
return contextName(c.c) + ".WithoutCancel"
Contains the standard definition of the context interface. Returns a copy of the parent context that will not be canceled when the parent context is canceled.
WithoutCancel
func WithoutCancel(parent Context) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
return withoutCancelCtx{parent}
}
In this same function we create our instance. Here are examples of use:
Logging – Suppose you have a service that runs long database queries and simultaneously logs those queries. You want the logging process to complete even if the query itself is canceled.
Sometimes you want to cache the results of an operation to speed up subsequent calls. Even if the operation is canceled, you want to keep the results in the cache so they can be used later.
WithoutCancel
useful in situations where certain operations must complete regardless of the state of the parent context. This can be useful for background tasks, logging, caching, and any other tasks that must complete even if the main operation is canceled, such as a rollback operation.
The context does not return Deadline
or Err
. Channel value Done
— nil. Reading from it will cause the program to lock.
Conclusion
I hope I helped you understand a little bit about the internal representation of how context works. Again, to better understand how it works, it’s better to delve into it, you just need to poke around with your hands source and write some unusual cases of using contexts in practice.