Skip to content

context

context назван как контекст, и его первоначальная цель дизайна — передача сигналов и некоторых простых данных через несколько goroutine, особенно между родительскими и дочерними goroutine. Он обычно используется в таких сценариях, как обработка HTTP-запросов, планирование задач, запросы к базам данных и т.д. Особенно в архитектурах микросервисов gRPC использует context для межпроцессной и межсетевой передачи метаданных, операций управления ссылками и т.д.

go
package main

import (
    "context"
    "fmt"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
    defer stop()
    for {
       select {
       case <-ctx.Done():
          fmt.Println("terminate")
          return
       default:
       }
       fmt.Println("running")
       time.Sleep(100 * time.Millisecond)
    }
}

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

Структура

context.Context — это не конкретная реализация, а интерфейс, определяющий набор методов:

go
// Методы Context могут вызываться несколькими goroutine одновременно.
type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key any) any
}
  • Deadline(), возвращает дедлайн и булево значение, указывающее, завершился ли он
  • Done(), возвращает канал, используемый для уведомления о завершении
  • Err(), возвращает причину закрытия context
  • Value(), получает указанное значение согласно ключу

В стандартной библиотеке есть следующие используемые contexts:

  • Background, пустой context, обычно используется как корневой узел contexts
  • WithCancel, WithCancelCause, отменяемый context
  • WithDeadline, WithDeadlineCause, context с дедлайном
  • WithTimeout, WithTimeoutCause, context с таймаутом
  • WithValue, context, который может переносить значения

Конкретно, основные реализации следующие:

  • timerCtx
  • cancelCtx
  • emptyCtx

Таким образом, его основная функциональность имеет четыре пункта:

  • Отмена
  • Дедлайн
  • Передача значений
  • Распространение

Понимание этих пунктов в основном проясняет, как работает context.

Отмена

go
type cancelCtx struct {
	Context

	mu       sync.Mutex            // защищает следующие поля
	done     atomic.Value          // от chan struct{}, создаётся лениво, закрывается первым вызовом cancel
	children map[canceler]struct{} // устанавливается в nil первым вызовом cancel
	err      error                 // устанавливается в non-nil первым вызовом cancel
	cause    error                 // устанавливается в non-nil первым вызовом cancel
}


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

Ядро cancelCtx лежит в методе propagateCancel, который отвечает за распространение отменяемого поведения на родительские и дочерние contexts.

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. Сначала проверяет, может ли родительский context быть отменён. Если нет, возвращается напрямую.

    go
    done := parent.Done()
    if done == nil {
    	return // родитель никогда не отменяется
    }
  2. Возвращает и проверяет, был ли родительский context уже отменён. Если да, отменяет все дочерние contexts.

    go
    select {
    case <-done:
    	// родитель уже отменён
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. Пытается преобразовать родительский context к типу cancelCtx. Если успешно, добавляет текущий context в children родительского context.

    go
    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
    }
  4. Пытается преобразовать его к типу afterFuncer. Если успешно, регистрирует метод для отмены текущего context в AfterFunc родительского context.

    go
    if a, ok := parent.(afterFuncer); ok {
    	// parent реализует метод 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
    }
  5. Если всё ещё не работает, запускает отдельную goroutine для прослушивания канала Done. Когда сигнал получен, отменяет дочерний context.

    go
    go func() {
        select {
        case <-parent.Done():
           child.cancel(false, parent.Err(), Cause(parent))
        case <-child.Done():
        }
    }()

Затем метод cancelCtx.cancel в конечном итоге отвечает за отмену дочерних contexts:

go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	...
}

Его поток следующий:

  1. Проверяет, был ли текущий context уже отменён.

    go
    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 // уже отменён
    }
  2. Закрывает канал done и отправляет уведомление о закрытии.

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. Обходит и уведомляет дочерние contexts.

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. Наконец, определяет, удалять ли из родительского context на основе переданного параметра.

    go
    if removeFromParent {
    	removeChild(c.Context, c)
    }

Дедлайн

WithTimeout и WithDeadline — это оба contexts с дедлайнами. Они одного типа, просто с разной семантикой, и оба основаны на cancelCtx.

go
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Под cancelCtx.mu.

	deadline time.Time
}

WithDeadlineCause отвечает за создание contexts с дедлайнами:

go
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	...
}

Его поток следующий:

  1. Проверяет дедлайн. Если родительский дедлайн раньше текущего дедлайна, тогда родительский context определённо будет отменён до текущего context, поэтому напрямую создаёт context типа cancelCtx.

    go
    if parent == nil {
    	panic("cannot create context from nil parent")
    }
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
    	// Текущий дедлайн уже раньше нового.
    	return WithCancel(parent)
    }
  2. Строит timerCtx и распространяет на дочерние contexts.

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. Вычисляет текущий дедлайн. Если уже истёк, отменяет напрямую.

    go
    dur := time.Until(d)
    if dur <= 0 {
    	c.cancel(true, DeadlineExceeded, cause) // дедлайн уже прошёл
    	return c, func() { c.cancel(false, Canceled, nil) }
    }
  4. Если не истёк, использует time.AfterFunc для установки отмены текущего context в дедлайн.

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

Для timerCtx его метод для отмены context просто останавливает timer и также останавливает дочерние contexts.

go
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	c.cancelCtx.cancel(false, err, cause)
	if removeFromParent {
		// Удаляем этот timerCtx из children родительского cancelCtx.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

Передача значений

valueCtx может передавать и получать значения в contexts:

go
type valueCtx struct {
    Context
    key, val any
}

func WithValue(parent Context, key, val any) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

Его ядро лежит в функции value. Другие конкретные реализации context также входят в эту функцию для получения значений, такие как:

go
func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

func (c *cancelCtx) Value(key any) any {
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

Функция value — это большой цикл for, который непрерывно рекурсирует вверх для поиска значения указанного ключа:

go
func value(c Context, key any) any {
	for {
		switch ctx := c.(type) {
		case *valueCtx:
			if key == ctx.key {
				return ctx.val
			}
			c = ctx.Context
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		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)
		}
	}
}

Обработка разных типов Context:

  • *valueCtx: Если key совпадает с key текущего Context, возвращает val, соответствующий key.
  • *cancelCtx: Если key совпадает с cancelCtxKey, возвращает текущий cancelCtx, т.е. возвращает себя.
  • withoutCancelCtx: Представляет Context без функциональности отмены. Если key совпадает с cancelCtxKey, возвращает nil.
  • *timerCtx: Если key совпадает с cancelCtxKey, возвращает его ассоциированный cancelCtx.
  • backgroundCtx и todoCtx: Обычно специальные типы Contexts, которые не переносят никаких дополнительных значений. При встрече с этими двумя типами напрямую возвращает nil.
  • Если неизвестный тип, продолжает вызывать метод Value для поиска.

Итоги

Ядро этих contexts — cancelCtx, который распространяет сигналы отмены. Другие типы context и даже сторонние типы context построены поверх этого, реализуя различные функциональности на основе этой основы.

Golang by www.golangdev.cn edit