context
context tem como tradução contexto, e foi projetado inicialmente para transmitir sinais e alguns dados simples entre múltiplas goroutines, especialmente entre goroutines pai e filho. É comumente usado em cenários como processamento de requisições HTTP, agendamento de tarefas, consultas de banco de dados, especialmente em arquiteturas de microsserviços, onde gRPC usa context para transmissão de metadados entre processos e redes, controle de rastreamento e outras operações.
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)
}
}No exemplo acima, context é usado para transmitir sinais de processo. Quando um sinal é recebido, o programa é encerrado automaticamente, o que é um dos cenários de uso do context.
Estrutura
context.Context não é uma implementação concreta, mas uma interface que define um conjunto de métodos:
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}Deadline(), retorna uma data limite e um valor booleano indicando se terminouDone(), retorna um canal usado para notificar términoErr(), retorna o motivo do fechamento do contextoValue(), obtém um valor específico baseado na chave
A biblioteca padrão possui os seguintes contextos utilizáveis:
Background, contexto vazio, geralmente usado como nó raiz de contextosWithCancel,WithCancelCause, contexto cancelávelWithDeadline,WithDeadlineCause, contexto com data limiteWithTimeout,WithTimeoutCause, contexto com tempo limiteWithValue, contexto que pode transmitir valores
As implementações principais são:
timerCtxcancelCtxemptyCtx
Portanto, suas funcionalidades principais são quatro pontos:
- Cancelamento
- Data limite
- Transmissão de valores
- Propagação
Entender estes pontos ajuda a compreender o funcionamento do context.
Cancelamento
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
cause error // set to non-nil by the first cancel call
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}O núcleo do cancelCtx está no método propagateCancel, que é responsável por propagar o comportamento de cancelamento para contextos pai e filho.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}Primeiro verifica se o contexto pai pode ser cancelado. Se não puder, retorna diretamente.
godone := parent.Done() if done == nil { return // parent is never canceled }Verifica se o contexto pai já foi cancelado. Se já foi, cancela todos os contextos filhos.
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }Tenta converter o contexto pai para tipo
cancelCtx. Se tiver sucesso, adiciona o contexto atual aoschildrendo contexto pai.goif p, ok := parentCancelCtx(parent); ok { // parent is a *cancelCtx, or derives from one. p.mu.Lock() if p.err != nil { // parent has already been canceled 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 }Tenta converter para tipo
afterFuncer. Se tiver sucesso, registra o método de cancelamento do contexto atual noAfterFuncdo contexto pai.goif a, ok := parent.(afterFuncer); ok { // parent implements an AfterFunc method. 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 }Se ainda não funcionar, cria uma goroutine separada para monitorar o canal
Done. Quando receber o sinal, cancela o contexto filho.gogo func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
Então o método cancelCtx.cancel é finalmente responsável por cancelar os contextos filhos:
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}Seu fluxo é o seguinte:
Verifica se o contexto atual já foi cancelado.
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 // already canceled }Fecha o canal
done, enviando notificação de fechamento.goc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }Percorre e notifica os contextos filhos.
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()Finalmente, decide se precisa remover do contexto pai baseado no parâmetro de entrada.
if removeFromParent { removeChild(c.Context, c) }
Data Limite
WithTimeout e WithDeadline são contextos com data limite. Ambos são do mesmo tipo, diferindo apenas na semântica de uso, e ambos são baseados em cancelCtx:
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause é responsável por criar contextos com data limite:
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}Seu fluxo é o seguinte:
Verifica a data limite. Se a data limite do pai for anterior à data atual, o contexto pai certamente será cancelado antes do contexto atual, então cria diretamente um contexto do tipo
cancelCtx.goif parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) }Constrói
timerCtxe propaga para os contextos filhos.goc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)Calcula o tempo limite atual. Se já expirou, cancela diretamente.
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } }Se não expirou, usa
time.AfterFuncpara configurar o cancelamento do contexto atual no tempo limite.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) }
Para timerCtx, seu método de cancelamento apenas para o timer e também para os contextos filhos:
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}Transmissão de Valores
valueCtx pode transmitir e obter valores em contextos:
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}
}Seu núcleo está na função value. Outras implementações de contexto também entram nesta função para obter valores, como:
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)
}A função value é um grande loop for que busca recursivamente valores de chave especificada subindo na hierarquia:
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 {
// This implements Cause(ctx) == nil
// when ctx is created using 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)
}
}
}Processamento de diferentes tipos de Context:
*valueCtx: Sekeycorresponder aokeydo Context atual, retorna ovalcorrespondente.*cancelCtx: Sekeycorresponder acancelCtxKey, retorna ocancelCtxatual, indicando retorno de si mesmo.withoutCancelCtx: Representa umContextsem função de cancelamento. Sekeycorresponder acancelCtxKey, retornanil.*timerCtx: Sekeycorresponder acancelCtxKey, retorna ocancelCtxassociado.backgroundCtxetodoCtx: São tipos especiais deContextque não carregam valores extras. Retornanilpara estes tipos.- Se for tipo desconhecido, continua chamando o método
Valuepara buscar.
Resumo
O núcleo destes contextos é cancelCtx, que propaga sinais de cancelamento. Outros tipos de contexto e tipos de contexto de terceiros também são construídos em camadas sobre esta base, implementando diversas funcionalidades.
