Skip to content

context

context tradotto come contesto, è stato progettato inizialmente per trasmettere segnali e alcuni dati semplici tra più goroutine, specialmente tra goroutine padre e figlio. Viene comunemente utilizzato nella gestione di richieste HTTP, schedulazione di task, query di database e altri scenari, specialmente nell'architettura dei microservizi, dove gRPC utilizza context per la trasmissione di metadati tra processi e reti, controllo della catena di chiamate e altre operazioni.

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

In questo esempio, context viene utilizzato per trasmettere i segnali del processo. Quando viene ricevuto un segnale, il programma si chiude automaticamente. Questo è uno degli scenari d'uso di context.

Struttura

context.Context non è un'implementazione concreta, ma un'interfaccia che definisce un insieme di metodi:

go
// 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(), restituisce una data di scadenza e un valore booleano che indica se è terminato
  • Done(), restituisce un channel utilizzato per notificare il messaggio di terminazione
  • Err(), restituisce il motivo della chiusura del context
  • Value(), restituisce il valore specificato in base alla chiave

Nella libreria standard ci sono i seguenti context utilizzabili:

  • Background, context vuoto, comunemente utilizzato come nodo radice dei context
  • WithCancel, WithCancelCause, context cancellabile
  • WithDeadline, WithDeadlineCause, context con data di scadenza
  • WithTimeout, WithTimeoutCause, context con timeout
  • WithValue, context che può trasmettere valori

Le implementazioni concrete sono principalmente:

  • timerCtx
  • cancelCtx
  • emptyCtx

Quindi le sue funzionalità principali sono quattro punti:

  • Cancellazione
  • Scadenza
  • Trasmissione di valori
  • Propagazione

Comprendendo questi punti, si può capire il principio di funzionamento di context.

Cancellazione

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

Il cuore di cancelCtx risiede nel metodo propagateCancel, che è responsabile della propagazione del comportamento di cancellazione ai context padre e figlio.

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. Per prima cosa verifica se il context padre può essere cancellato, se non può, ritorna direttamente.

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. Verifica se il context padre è già stato cancellato, se sì, cancella tutti i context figlio.

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. Tenta di convertire il context padre in tipo cancelCtx, se la conversione ha successo, aggiunge il context corrente ai children del context padre.

    go
    if 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
    }
  4. Tenta di convertirlo in tipo afterFuncer, se la conversione ha successo, registra il metodo di cancellazione del context corrente nell'AfterFunc del context padre.

    go
    if 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
    }
  5. Se ancora non funziona, avvia una goroutine separata per ascoltare il channel Done, quando riceve il segnale, cancella il context figlio.

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

Poi il metodo cancelCtx.cancel è responsabile della cancellazione finale dei context figlio.

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

Il suo flusso è il seguente:

  1. Verifica se il context corrente è già stato cancellato.

    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 // already canceled
    }
  2. Chiude il channel done, invia la notifica di chiusura.

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. Itera per notificare i context figlio.

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. Infine, in base al parametro传入, decide se rimuovere dal context padre.

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

Scadenza

Sia WithTimeout che WithDeadline sono context con scadenza, entrambi sono dello stesso tipo, differiscono solo semanticamente, e sono entrambi basati su cancelCtx.

go
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

WithDeadlineCause è responsabile della creazione di context con scadenza.

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

Il suo flusso è il seguente:

  1. Verifica la data di scadenza, se la data di scadenza del padre è precedente alla data di scadenza corrente, allora il context padre verrà sicuramente cancellato prima del context corrente, quindi crea direttamente un context di tipo cancelCtx.

    go
    if 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)
    }
  2. Costruisce timerCtx e propaga ai context figlio.

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. Calcola il tempo di scadenza corrente, se è già scaduto, lo cancella direttamente.

    go
    dur := time.Until(d)
    if dur <= 0 {
    	c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
    	return c, func() { c.cancel(false, Canceled, nil) }
    }
  4. Se non è scaduto, utilizza time.AfterFunc per impostare la cancellazione del context corrente al tempo di scadenza.

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

Per timerCtx, il suo metodo di cancellazione del context si limita a fermare il timer, e顺便 ferma i context figlio.

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

Trasmissione di Valori

valueCtx può trasmettere e ottenere valori nei context.

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

Il suo cuore risiede nella funzione value, anche le altre implementazioni concrete di context entrano in questa funzione per ottenere i valori, ad esempio:

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

La funzione value è un grande ciclo for, che cerca ricorsivamente verso l'alto il valore della chiave specificata.

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

Gestione di diversi tipi Context:

  • *valueCtx: Se la key corrisponde alla key del Context corrente, restituisce il val corrispondente alla key.
  • *cancelCtx: Se la key corrisponde a cancelCtxKey, restituisce il cancelCtx corrente, indicando se stesso.
  • withoutCancelCtx: Rappresenta un Context senza funzionalità di cancellazione, se la key corrisponde a cancelCtxKey, restituisce nil.
  • *timerCtx: Se la key corrisponde a cancelCtxKey, restituisce il cancelCtx associato.
  • backgroundCtx e todoCtx: Sono tipi speciali di Context che non contengono valori extra, restituiscono direttamente nil quando incontrati.
  • Se è un tipo sconosciuto, continua a chiamare il metodo Value per cercare.

Questi context hanno come cuore cancelCtx, che propaga i segnali di cancellazione. Gli altri tipi di context, inclusi alcuni context di terze parti, sono stratificati uno sull'altro, implementando varie funzionalità su questa base.

Golang by www.golangdev.cn edit