Contextとは
- タイムアウト処理やgoroutineの処理停止を統一的に扱うためのパッケージ
- 1.7から標準パッケージとして導入された
- 提供されている機能としては以下の2つ
- コールグラフの各枝をキャンセルするAPI
- この記事ではこれについて扱う
- コールグラフを通じてリクエストに関するデータを渡すデータの置き場所
- コールグラフの各枝をキャンセルするAPI
Contextパッケージのソースコード
- 主にcontext.goの内容を深掘りしていく
インターフェース
- context.go#L58-L154
- 定義は以下のようになっている
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
-
Done()
- 完了を知らせるチャンネル
- 完了している場合はこのチャンネルから値を呼び出せるようになる
- 以下のようにselectの中で使うことが想定されている
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx) |
if err != nil { |
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
}
-
Err()
- 完了している場合、Err()に完了理由が設定される
- 完了理由はcontext.go#L156-L157で以下のように定義されている
var Canceled = errors.New("context canceled")
外部から呼び出される関数
- 全部で6つの関数があり、3種類に分けられる
Contextを生成する関数
-
Background()
- context.go#L204-L210
- これを呼び出すことでContextが生成される
- 実態は
emptyCtx
と名付けられたint型 - インターフェースを満たしている
- 名前通り空のContextである
- 実態は
- 全てはここから始める
-
TODO()
- context.go#L212-L218
-
Background()
と同様に空のContextを生成するが、実装途中などで使用するContextが定まってない場合に暫定的に使うもの
キャンセル処理に関する関数
-
WithCancel()
- context.go#L226-L239
- Contextを受け取り、Contextと
cancel()
関数を返す- この
cancel()
を実行することでContextのDoneチャンネルが閉じられる(Done()
チャンネルから値を呼び出せるようになる)
- この
-
cancelCtx
というContextを生成し、それを用いてpropagateCancel()
関数を呼び出す-
propagateCancel()
は親のCotextがキャンセルされていた場合に文字通り子Cotextにもキャンセルを伝搬させる働きがある
-
-
WithDeadline()
- context.go#L421-L456
- 基本的には
WithCancel()
と同じだが、受け取る引数に時刻が追加されている - 戻り値となっている
cancel()
は、指定された時刻になったときに実行されるようになっている-
time.AfterFunc
でcancel()
を指定している - context.go#L444-L447
-
-
timerCtx
というContextを生成し、それを用いてpropagateCancel()
関数を呼び出す
-
WithTimeout()
- context.go#L492-L504
- 基本的には
WithDeadline()
と同じだが、受け取る引数が時間になっている(タイムアウトさせるまでの時間) - コードを読むと自明だが、現在時刻に受け取った時間を足し、
WithDeadline()
を呼び出しているだけ
-
WithValue()
- context.go#L506-L530
- 深入りしないが、Contextで値を渡すときに使用するもの
- 使用する例として、ヘッダーから抽出されたユーザーID、CookieまたはセッションIDに関連付けられた認証トークンなどが挙げられている
cancel()
について
- context.go#L392-L419
-
WithCancel()
、WithDeadline()
、WithTimeout()
のいずれからも呼ばれるcancel()
を深掘りしてみる - ソースコードは以下の通り
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
- 6行目
if c.err != nil
-
err
には処理が完了した場合にその理由が詰められる - そのためnilでない場合はすでに完了している場合なので、そのままreturnしている
-
- 10行目
c.err = err
- ここで完了理由を設定するS
- 12行目
c.done = closedchan
-
done
にclosedchan
(チャンネルのスライス)を設定する - これにより
Done()
チャンネルから値が呼び出せるようになる
-
- 16行目〜
for child := range c.children
- 子どものContext分だけfor文をまわし、その中で**
cancel()
を再起的に呼び出している** - これにより全ての処理を完了させることができる
- 子どものContext分だけfor文をまわし、その中で**
- 24行目
removeChild(c.Context, c)
- 親のContextから子どものContextを切り離す
使い方
-
Go言語による並行処理の4章 Goでの並行処理パターン 4.12 contextパッケージで紹介されているソースコード(P139,140)を抜粋して用いる
- 元のコードでは
main()
からprintGreeting()
とprintFarewell()
を呼び出しているが、printGreeting()
に関する部分のみを抜粋した
- 元のコードでは
- このコードでは1秒でタイムアウトが発生し処理がキャンセルされるようになっている
- 出力結果は以下の通り
cannot print greeting: context canceled
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
// context.Background()でContextを生成し、WithCancelに渡す
// 戻り値は新たに生成されたCotextとcancel()関数
ctx, cancel := context.WithCancel(context.Background())
// 必ずキャンセル処理が実行されるようdeferに指定しておく
defer cancel()
wg.Add(1)
go func() {
defer wg.Done()
// 並行処理にContextを渡す
if err := printGreeting(ctx); err != nil {
fmt.Printf("cannot print greeting: %v\n", err)
// printGreeting()からエラーが返ってきたらContextをキャンセルする
cancel()
}
}()
wg.Wait()
}
func printGreeting(ctx context.Context) error {
// 後続の処理にContextを渡す
greeting, err := genGreeting(ctx)
if err != nil {
return err
}
fmt.Printf("%s world!\n", greeting)
return nil
}
func genGreeting(ctx context.Context) (string, error) {
// 親のContextをWithTimeout()に渡してContextを再生成する
// このctxを受け取るlocal()は1秒でタイムアウトするので、1秒後にcancel()が実行される
// ここで設定しているキャンセル処理は親のContextには影響を与えない
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
switch locale, err := locale(ctx); {
case err != nil:
return "", err
case locale == "EN/US":
return "hello", nil
}
return "", fmt.Errorf("unsupported locale")
}
func locale(ctx context.Context) (string, error) {
select {
// タイムアウトが発生するとctx.Done()から値を呼び出すことができ、ここでctx.Err()で完了理由を返す
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(1 * time.Minute):
}
return "EN/US", nil
}