context
context назван как контекст, и его первоначальная цель дизайна — передача сигналов и некоторых простых данных через несколько goroutine, особенно между родительскими и дочерними goroutine. Он обычно используется в таких сценариях, как обработка HTTP-запросов, планирование задач, запросы к базам данных и т.д. Особенно в архитектурах микросервисов gRPC использует context для межпроцессной и межсетевой передачи метаданных, операций управления ссылками и т.д.
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 — это не конкретная реализация, а интерфейс, определяющий набор методов:
// Методы Context могут вызываться несколькими goroutine одновременно.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}Deadline(), возвращает дедлайн и булево значение, указывающее, завершился ли онDone(), возвращает канал, используемый для уведомления о завершенииErr(), возвращает причину закрытия contextValue(), получает указанное значение согласно ключу
В стандартной библиотеке есть следующие используемые contexts:
Background, пустой context, обычно используется как корневой узел contextsWithCancel,WithCancelCause, отменяемый contextWithDeadline,WithDeadlineCause, context с дедлайномWithTimeout,WithTimeoutCause, context с таймаутомWithValue, context, который может переносить значения
Конкретно, основные реализации следующие:
timerCtxcancelCtxemptyCtx
Таким образом, его основная функциональность имеет четыре пункта:
- Отмена
- Дедлайн
- Передача значений
- Распространение
Понимание этих пунктов в основном проясняет, как работает context.
Отмена
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.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}Сначала проверяет, может ли родительский context быть отменён. Если нет, возвращается напрямую.
godone := parent.Done() if done == nil { return // родитель никогда не отменяется }Возвращает и проверяет, был ли родительский context уже отменён. Если да, отменяет все дочерние contexts.
goselect { case <-done: // родитель уже отменён child.cancel(false, parent.Err(), Cause(parent)) return default: }Пытается преобразовать родительский context к типу
cancelCtx. Если успешно, добавляет текущий context вchildrenродительского context.goif 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 }Пытается преобразовать его к типу
afterFuncer. Если успешно, регистрирует метод для отмены текущего context вAfterFuncродительского context.goif 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 }Если всё ещё не работает, запускает отдельную goroutine для прослушивания канала
Done. Когда сигнал получен, отменяет дочерний context.gogo func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
Затем метод cancelCtx.cancel в конечном итоге отвечает за отмену дочерних contexts:
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}Его поток следующий:
Проверяет, был ли текущий context уже отменён.
goif 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 // уже отменён }Закрывает канал
doneи отправляет уведомление о закрытии.goc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }Обходит и уведомляет дочерние contexts.
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()Наконец, определяет, удалять ли из родительского context на основе переданного параметра.
goif removeFromParent { removeChild(c.Context, c) }
Дедлайн
WithTimeout и WithDeadline — это оба contexts с дедлайнами. Они одного типа, просто с разной семантикой, и оба основаны на cancelCtx.
type timerCtx struct {
cancelCtx
timer *time.Timer // Под cancelCtx.mu.
deadline time.Time
}WithDeadlineCause отвечает за создание contexts с дедлайнами:
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}Его поток следующий:
Проверяет дедлайн. Если родительский дедлайн раньше текущего дедлайна, тогда родительский context определённо будет отменён до текущего context, поэтому напрямую создаёт context типа
cancelCtx.goif parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // Текущий дедлайн уже раньше нового. return WithCancel(parent) }Строит
timerCtxи распространяет на дочерние contexts.goc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)Вычисляет текущий дедлайн. Если уже истёк, отменяет напрямую.
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // дедлайн уже прошёл return c, func() { c.cancel(false, Canceled, nil) } }Если не истёк, использует
time.AfterFuncдля установки отмены текущего context в дедлайн.goc.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.
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:
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 также входят в эту функцию для получения значений, такие как:
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, который непрерывно рекурсирует вверх для поиска значения указанного ключа:
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 построены поверх этого, реализуя различные функциональности на основе этой основы.
