context
context はコンテキストと訳され、その設計の初衷は複数のコルーチン、特に親子コルーチン間で信号といくつかのシンプルなデータを伝達することです。これは 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 の 1 つの使用シーンです。
構造
context.Context は具体的な実装ではなく、一連のメソッドを定義したインターフェースです。
// 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()、期日を返し、ブール値はすでに終了したかどうかを示しますDone()、終了通知に使用されるチャネルを返しますErr()、コンテキストが閉じられた理由を返しますValue()、key に基づいて指定された値を取得します
標準ライブラリには合計で以下のいくつかの使用可能なコンテキストがあります。
Background、空白のコンテキストで、通常はコンテキストのルートノードとして使用されますWithCancel、WithCancelCause、キャンセル可能なコンテキストWithDeadline、WithDeadlineCause、期日付きのコンテキストWithTimeout、WithTimeoutCause、タイムアウト時間付きのコンテキストWithValue、値を伝達できるコンテキスト
実装としては主に以下のものがあります。
timerCtxcancelCtxemptyCtx
したがって核心機能は 4 つのポイントです。
- キャンセル
- 期日
- 値伝達
- 伝播
これらを理解すれば、基本的に context の動作原理がわかります。
キャンセル
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
}cancelCtx の核心は propagateCancel メソッドにあり、これはキャンセル可能な動作を親子コンテキストに伝播する責任を負います。
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}まず親 context がキャンセル可能かどうかをチェックします。できない場合は直接戻ります。
godone := parent.Done() if done == nil { return // parent is never canceled }親 context がすでにキャンセルされているかどうかをチェックします。はいの場合はすべての子 context をキャンセルします。
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }親 context を
cancelCtxタイプに変換しようとします。成功した場合は現在の context を親 context のchildrenに追加します。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 }afterFuncerタイプに変換しようとします。成功した場合は、現在の context をキャンセルするメソッドを親 context のAfterFuncに登録します。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 }それでもダメな場合は、単独でコルーチンを開始して
Doneチャネルを監視します。信号を受信した際に子 context をキャンセルします。gogo func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
その後 cancelCtx.cancel メソッドが最終的に子 context をキャンセルする責任を負います。
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 // already canceled }doneチャネルを閉じ、終了通知を送信します。goc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }循環して子 context に通知します。
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()最後に伝われたパラメータに基づいて親 context から削除する必要があるかどうかを判断します。
if removeFromParent { removeChild(c.Context, c) }
期日
WithTimeout と WithDeadline はどちらも期日付きのコンテキストで、両者は同じタイプです。使用されるセマンティクスが異なるだけで、どちらも cancelCtx に基づいています。
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause は期日付きのコンテキストを作成する責任を負います。
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}そのフローは以下の通りです。
期日をチェックします。親の期日が現在の期日よりも早い場合、親 context は現在の context よりも先にキャンセルされるため、直接
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) }timerCtxを構築し、子 context に伝播します。goc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)現在の截止时间を計算します。すでに截止している場合は直接キャンセルします。
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed 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 にとって、コンテキストをキャンセルするメソッドは timer を停止し、ついでに子 context も停止するだけです。
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()
}値伝達
valueCtx はコンテキスト内で値を伝達・取得できます。
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 ループで、絶えず上に再帰して指定された key の値を探します。
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)
}
}
}異なる Context タイプの処理:
*valueCtx:keyが現在の Context のkeyにマッチする場合、keyに対応するvalを返します。*cancelCtx:keyがcancelCtxKeyにマッチする場合、現在のcancelCtxを返します。つまり自身を返します。withoutCancelCtx:キャンセル機能のないContextを表します。keyがcancelCtxKeyにマッチする場合、nilを返します。*timerCtx:keyがcancelCtxKeyにマッチする場合、それに関連付けられたcancelCtxを返します。backgroundCtxとtodoCtx:通常は追加の値を携带していない特殊なタイプのContextです。これら 2 つのタイプに遭遇した場合は直接nilを返します。- 未知のタイプの場合は、
Valueメソッドを呼び出して探し続けます。
まとめ
これらの context の核心は cancelCtx で、これを通じてキャンセル信号を伝播します。他の context タイプおよびサードパーティの context タイプも 1 層ずつ重ねて、この基础上でさまざまな機能を実装しています。
