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 canceled

  • TODO() Context – used as a placeholder if you haven't yet defined what context you need and you will redefine it

  • WithCancel(parent Context) (ctx Context, cancel CancelFunc) – creates a child context with a cancel method from the parent context that can be called manually

  • WithDeadline(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 reached

  • context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) – the same as WithDeadlineexcept that it specifies the timeout from the current time

  • WithValue(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 withValueWhen 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 returns

  • Successive 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 nil

  • If 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 expired

  • After Err returns a non-zero error, subsequent calls Err 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 result

  • The 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 WithCancelCausebecause 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 backgroundCtxwhich 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 cancelCtxinternal alarm cancelCtx depends on the channel DoneIf 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 than d.

  • If the parent context's deadline is already set to d, WithDeadline(parent, d) semantically equivalent parent.

  • Returned channel Context.Donewhich is closed when the timer expires, when the function is called cancel or when the parent context channel terminates closedwhichever 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 Contextwhich 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. Oncewhich 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *