Вступление

Ни для кого не секрет, что стандартный пакет context широко используется для создания ваших сервисов. В данный момент, не побоюсь этого слова, любой сервис написанный на Go использует контексты повсеместно. Мое мнение таково - если ты хочешь прогрессировать как специалист, ты должен копать все глубже и глубже. Предлагаю рассмотреть context с призмы его работы внутри.

Определение Сontext

Существует несколько типов context, с которыми Golang разработчику приходится сталкиваться. Давайте кратко пробежимся и потом вникнем в суть каждого. Но сначала обратим внимание на функции создания контекста.

  • context.Background() Context - используется для создания корневого контекста и не может быть отменен

  • TODO() Context - используется как заглушка, если вы еще не определили какой контекст вам нужен и вы его переопределите

  • WithCancel(parent Context) (ctx Context, cancel CancelFunc) - создает дочерний контекст с методом отмены из родительского контекста, который может быть вызван вручную

  • WithDeadline(parent Context, d time.Time) (Context, CancelFunc) - создает дочерний контекст с помощью метода отмены из родительского контекста, за исключением того, что контекст будет автоматически отменен по достижении заданного времени

  • context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) - то же самое, что и WithDeadline, за исключением того, что он указывает время ожидания от текущего времени

  • WithValue(parent Context, key, val any) Context - создает дочерний контекст из родительского контекста, который может хранить пару ключ-значение и является контекстом и также его нельзя отменить

Само по себе определение Context лежит в пакете context и это является интерфейсом

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}

И можно прочесть описание этого интерфейса

A Context carries a deadline, a cancellation signal, and other values across API boundaries. Context's methods may be called by multiple goroutines simultaneously.

Также можно отметить, что входящие запросы к серверу должны создавать контекст, а исходящие вызовы к серверам должны принимать его. Цепочка функций вызовов между ними должна распространять контекст, при необходимости заменяя его с производным контекстом, созданным с помощью WithCancel, WithDeadline, withTimeout или withValue. Когда контекст отменяется, все контексты, производные от него, также отменяются.

Что означает, что мы должны принимать context и далее по цепочке вызовов его переопределять, так как context.Background() должен являться по сути начальным контекстом, где дальше идут ответвления.

Можно увидеть совет, который спрятан в описании пакета

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 ...
}

И если дословно перевести, то это означает, что не надо хранить контексты внутри структуры, лучше их передавать функции, которая нуждается в нем, также можно заметить примечание, что следует передавать его первым параметром и обычно называть его ctx. Обычно эта ошибка встречается у новичков, которые только входят в мир Golang, что очень сказывается на читаемости кода.

Ну и можно выделить совсем уж простой совет, что нужно передавать context только в области данных запроса, а не в функцию или метод необязательным параметром.

Deadline

Сам по себе метод Deadline возвращает время, когда задача была выполнена от имени текущего контекста. А если крайний срок не установлен, то возвращает значение ok, равное false. При последовательных вызовах функции Deadline результаты будут одинаковыми. Что полезно знать. как реализуют разные типы контекстов этот метод, мы посмотрим чуть позже.

Done

Давайте разберем поближе этот метод.

  • Когда работа, выполняемая от имени контекста, должна быть отменена, Done возвращает канал, который закрыт

  • Если этот контекст никогда не может быть отменен, Done возвращает nil

  • Закрытие канала Done может произойти асинхронно после возврата функции cancel

  • Последовательные вызовы Done возвращают одно и то же значение

И что самое удобное - мы можем использовать Done в 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

C ним не сложнее чем с Done.

  • Если параметр Done еще не закрыт, Err возвращает значение nil

  • Если параметр Done закрыт, Err возвращает ненулевую ошибку, объясняющую причину почему отменено, если контекст был отменен или отменено по истечении срока действия, если истек срок действия контекста

  • После того, как Err возвращает ненулевую ошибку, последующие вызовы Err возвращают ту же ошибку

Value

Тут уже поинтереснее.

  • Возвращает значение, связанное с этим контекстом для key, или nil, если с key не связано ни одно значение

  • Последовательные вызовы Value с помощью одного и того же ключа возвращают один и тот же результат

  • Ключ может быть любого типа, поддерживающего равенство - пакеты должны определять ключи как неэкспортируемый тип, чтобы избежать коллизий

  • Ключ идентифицирует конкретное значение в контексте

  • Пакеты, определяющие контекстный ключ, должны предоставлять типобезопасные средства доступа для значений, сохраненных с использованием этого ключа

Давайте разберем последнее утверждение. Хорошей практикой является создание отдельного типа ключа и доступа к значению к контексте по нему, причем ключ должен быть неэкспортируемый, так как это нужно, чтобы избежать конфликтов ключей с другими пакетами, например.

// Создаем ключ для контекста
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
}

Важное замечание! Передача значений по контексту является плохой практикой. Передавайте значения по параметрам, используйте передачу по контексту только в вынужденных ситуациях. Например, можно использовать передачу по контексту логгера, какой-либо middleware айдишник, но опять же, все решения должны быть продуманы. Есть несколько причина этому, но одна из них это то, что при большой вложенности страдает перебор контекстов, также ухудшается документирование, создается проблема синхронизации работы команды (так как если мы будем использовать контекст как хранилище, то кто знает, вдруг по пути данные нечаянно потеряются незаметно и все :) )

Можно разобрать функцию 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 возвращает ненулевую ошибку, которая объясняет, почему был отменен контекст. Причина отмены — это первая отмена контекста или одного из его родительских элементов. Если отмена произошла с помощью CancelCauseFunc(err), то Cause возвращает значение ошибки. В противном случае — значение c.Err(). Если контекст еще не был отменен, Cause возвращает значение nil.

Примечание: Мы знаем, что контекст не является потомком какого-либо контекста, созданного с помощью WithCancelCause, так как у него нет значения cancelCtxKey. Если это не один из стандартных типов контекста, может возникнуть ошибка, даже без причины.

Само использование мы рассмотрим ниже в статье.

Внутренние типы context

emptyCtx

emptyCtx - это структура, которая никогда не отменяется, возращает nil всеми ее методами, не имеет значений и не имеет крайнего срока, также реализует интерфейс Context. Используется для создания корневого контекста, который возвращает context.Background() и context.TODO() в стандартной библиотеке.

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"
}

Также мы можем увидеть определение методов String() для todoCtx и backgroundCtx, что возращает именное представление TODO и Background.

cancelCtx

Это контекст, который может быть отменен, также при отмене отменяются все его дочерние элементы, которые реализуют функцию отмены. Ее создает 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 определяется как контекст, который может быть отменен. Из-за древовидной структуры контекста при отмене все дочерние контексты должны быть отменены синхронно. Вам нужно просто пройти по структуре children map[canceler]structure{} и отменить их по одному.

Что же по сути является canceler? Это просто интерфейс с определенными полями. Его имплементацией являются *timerCtx и *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"
}

Мы видим обычное определение интерфейса Context, где внутри просто все синхронизировано мьютексами для безопасного доступа к каналу. Но более интереснее следующий метод.

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 организует отмену дочернего элемента при наличии родительского, устанавливает родительский контекст cancelCtx . Таким образом, метод propagateCancel гарантирует, что отмена родительского контекста будет корректно передана всем дочерним контекстам, обеспечивая согласованность и упрощая управление временем жизни связанных операций.

Как видно из исходного кода создания cancelCtx, внутренняя сигнализация cancelCtx зависит от канала Done. Если вы хотите отменить этот контекст, вам нужно заблокировать все <-c.Done (). Самый простой способ — закрыть этот канал или заменить его уже закрытым каналом.

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)
	}
}

В данном методе мы обеспечиваем корректную отмену текущего контекста и всех его дочерних контекстов, гарантируя, что все связанные с ними операции будут надлежащим образом завершены.

Можно посмотреть для большей ясности определение 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()
}

В целом логика должна быть понятна, единственное, определение stopCtx мы разберем ниже в статье.

WithCancel

Создается же этот контекст с помощью 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 возвращает копию родителя с новым каналом Done. Канал Done контекста закрывается при вызове функции отмены или при закрытии канала Done родительского контекста. Отмена этого контекста освобождает ресурсы, поэтому код должен вызывать cancel после завершения операций в этом контексте.

Также отмену нужно вызывать на уровне, где она была создана. Вызывать ее в другом месте - антипаттерн, так как это может приводить к утечке незакрытых контекстов.

CancelFunc является обычной функцией.

type CancelFunc func()

Функция CancelFunc информирует операцию о прекращении работы, но не ждет ее завершения. Ее можно вызывать одновременно несколькими подпрограммами. После первого вызова последующие вызовы ничего не делают.

Можем обратить внимание на withCancel().

func withCancel(parent Context) *cancelCtx {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := &cancelCtx{}
	c.propagateCancel(parent, c)
	return c
}

В ней ничего сверхъестественного нет, все, что разбирали выше тут выполняется.

timerCtx

Он построен поверх cancelCtx. Единственное отличие в добавлении таймера и времени отсечки. С помощью этих двух конфигураций можно автоматически отменить таймер в определенное время с помощью методов Deadline и 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()
}

То есть тут определение достаточно простое и понятное.

WithDeadline

Чтобы рассмотреть как создается наш timerCtx , обратим внимание на WithDeadline .

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	return WithDeadlineCause(parent, d, nil)
}

Просмотрим основные положения:

  • WithDeadline возвращает копию родительского контекста с измененным сроком, который должен быть установлен не позднее d.

  • Если срок для родительского контекста уже установлен до d, WithDeadline(parent, d) семантически эквивалентен parent.

  • Возращенный канал Context.Done, который закрывается по истечении срока, при вызове функции cancel или при завершении канала родительского контекста closed, в зависимости от того, что произойдет раньше.

  • Отмена этого контекста высвобождает связанные с ним ресурсы, поэтому код должен отменить вызов, как только завершатся операции, выполняемые в этом.

Мы возращаем 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

Он содержит экземпляр интерфейса Context, который является дочерним контекстом, и два поля key, val interface {}. При вызове valueCtx.Value(key interface ()) выполняется рекурсивный поиск. Он отвечает только за поиск контекста. Невозможно узнать, содержит ли родственный контекст этот ключ.

type valueCtx struct {
	Context
	key, val any
}

Он имеет всего 2 метода.

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)
}

Ну и само определение 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)
		}
	}
}

Метод рекурсивно проходит по всей цепочке контекстов и возвращает значение, связанное с указанным ключом, если такое значение найдено. Если ключ не найден ни в одном из контекстов, возвращается nil.

stopCtx

type stopCtx struct {
	Context
	stop func() bool
}

Используется в качестве родительского контекста для функции stop, которая отменяет регистрацию функции AfterFunc. Методов у нее нет.

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()
	})
}

У него только один метод и содержит контекст отмены и примитив синхронизации 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
	}
}

Функция AfterFunc принимает функцию, которая будет выполнена после завершения работы контекста, включая случаи истечения таймаута. Если контекст уже завершился, функция запустится немедленно. Выполнение функции происходит в отдельном потоке. При этом каждый вызов AfterFunc выполняется независимо от других.

AfterFunc возвращает функцию остановки. При вызове функции остановки разрывается связь между функцией и контекстом. Если контекст уже находится в состоянии Done и функция уже была запущена, или если функция уже была остановлена, то функция остановки возвращает false.

Функция завершает работу, если значение true. Функция stop не ожидает завершения работы функции, поэтому для контроля состояния рекомендуется явно взаимодействовать с ней.

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"

Содержит стандартное определение интерфейса контекста. возвращает копию родительского контекста, которая не будет отменена при отмене родительского контекста.

WithoutCancel

func WithoutCancel(parent Context) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	return withoutCancelCtx{parent}
}

В этой же функции создаем наш экзмепляр. Вот примеры использования:

  • Логгирование - Предположим, что у вас есть служба, которая выполняет длинные запросы к базе данных и одновременно логирует эти запросы. Вы хотите, чтобы процесс логирования завершился, даже если сам запрос отменен.

  • Иногда нужно кэшировать результаты операции, чтобы ускорить последующие вызовы. Даже если операция отменена, вы хотите сохранить результаты в кэше, чтобы они могли быть использованы позже.

WithoutCancel полезен в ситуациях, где определенные операции должны завершиться независимо от состояния родительского контекста. Это может быть полезно для фоновых задач, логирования, кэширования, и любых других задач, которые должны завершиться даже при отмене основной операции, например, rollback операции.

Контекст не возвращает Deadline или Err. Значение канала Done — nil. Чтение из него приведёт к блокировке программы.

Заключение

Надеюсь я помог немного понять внутреннее представление о работе контекста. Опять же, чтобы лучше понять его работу, лучше вникнуться, нужно просто потыкать ручками исходный код и пописать какие-то необычные кейсы использования контекстов на практике.

Комментарии (2)


  1. Sly_tom_cat
    10.07.2024 22:16

    Честно говоря идея контекста WithoutCancel мне так и не ясна.
    Если есть функция, которой надо завершится не смотря на контекст, то зачем ей нужен этот контекст?

    Если в этой функции нужен контекст для отмены дочерних задач, то тут можно просто от context.Background создать контекст внутри этой функции и раздавать уже его/производные от него.

    Что такого дает связь с родительским если от него отмена не придет, а отмена в него по определению не распространяется?

    Мне просто казалось контекст тем и хорош, что получив родительский, на его основе можно наплодить сколько нужно своих, зная, что эти дочерние отменятся через cancel/по времени или когда отмена сверху прилетит. И именно такой сценарий чаще всего и нужен ИМХО.


    1. flew1x Автор
      10.07.2024 22:16

      Этот контекст может быть полезен, когда Вы используете контекст, но не хотите, чтобы его отмена повлияла на функцию или метод. Например, иногда необходимо передавать значения из родительского в новый, но нам нельзя его отменять, так как нужно сохранять доступ к значению. Сам контекст же достаточно специфичен в использовании. Ну или например нам нельзя отменять фоновую операцию заполнения кеша. Просто разграничиваем части сервиса, где это требуется. Если отменим базовый контекст, то фоновые процессы все еще будут продолжать работать