いまさら Go の context を少しだけ整理してみたメモ。
はじめに
Go で並行処理を実装するときはたいがい context を使うけど context はすごくよくできててフワッとした理解でもちゃんと使えてしまう。 いまさらだけど自分のなかで context とはなにかどうやって使うものかを少し整理したのでメモしておく。
context とは
context とはなにかだけど Go Concurrency Patterns: Context - The Go Blog の説明が分かりやすかったので引用しておく。
In Go servers, each incoming request is handled in its own goroutine. At Google, we developed a context package that makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request.
(訳) Go のサーバーは受信した各リクエストをそれぞれの goroutine で処理します。 Google はリクエストスコープの値、キャンセルシグナル、デッドラインを API の境界を越えてリクエストの処理に関わるすべての goroutine に簡単に渡すことができる context パッケージを開発しました。
あとこのへん。
A Context is safe for simultaneous use by multiple goroutines. Code can pass a single Context to any number of goroutines and cancel that Context to signal all of them.
(訳) context は複数の goroutine が同時に安全に使える。コードは単一の context を任意の数の goroutine に渡すことができ、その context をキャンセルするとシグナルをすべての goroutine に送ることができる。
context ができること
context は大雑把にはこういうことができる。
- 値を API の境界を超えて複数の goroutine に渡せる
- context は木構造にでき、任意の context をキャンセルするとその context とその context の枝に連なるすべての Context をキャンセルできる
context の使い方
context をキャンセルする
キャンセルできる context は context.WithCancel()
で作る。
context.WithCancel()
の2番目の返り値の関数を呼び出すと1番目の返り値の context をキャンセルできる。
コードはこんな感じ。
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) go func() { run(ctx) }() time.Sleep(5 * time.Second) cancel() time.Sleep(1 * time.Second) } func run(ctx context.Context) { i := 1 for { fmt.Printf("run(): %d count\n", i) i++ select { case <-time.After(1 * time.Second): continue case <-ctx.Done(): fmt.Printf("run(): canceled. reasone: %s\n", ctx.Err().Error()) return } } } // run(): 1 count // run(): 2 count // run(): 3 count // run(): 4 count // run(): 5 count // run(): canceled. reasone: context canceled
main()
関数は ctx, cancel := context.WithCancel(context.Background())
で context を作って run()
関数を新しい goroutine で動かす。
run()
関数は無限ループで1秒ごとに run(): 1 count
みたいな文字列を出力する。
run()
関数に渡した context がキャンセルすると ctx.Done()
が返すチャネルがクローズするので run()
関数 は return して終了する。
指定した時間が経過したら context をキャンセルする
指定した時間が経過したらキャンセルする context は context.WithTimeout()
で作る。
context は context.WithTimeout()
の2番目の引数に指定する時間が経過すると自動的にキャンセルする。
context.WithTimeout()
の2番目の返り値の関数を呼び出すと context が自動的にキャンセルする前に context をキャンセルできる。
コードはこんな感じ。
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() go func() { run(ctx) }() time.Sleep(10 * time.Second) } // run() 関数はさっきのと同じ // run(): 1 count // run(): 2 count // run(): 3 count // run(): 4 count // run(): 5 count // run(): 6 count // run(): canceled. reasone: context deadline exceeded
main()
関数は ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
で context を作って run()
関数を新しい goroutine で動かす。
run()
関数は無限ループで1秒ごとに run(): 1 count
みたいな文字列を出力する。
run()
関数に渡した context がキャンセルすると ctx.Done()
が返すチャネルがクローズするので run()
関数 は return して終了する。
指定した日時になったら context をキャンセルする
指定した日時になったらキャンセルする context は context.WithDeadline()
で作る。
context は context.WithDeadline()
の2番目の引数に指定する日時になると自動的にキャンセルする。
context.WithDeadline()
の2番目の返り値の関数を呼び出すと context が自動的にキャンセルする前に context をキャンセルできる。
コードはこんな感じ。
package main import ( "context" "fmt" "time" ) func main() { deadline := time.Now().Add(5 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() go func() { run(ctx) }() time.Sleep(10 * time.Second) } // run() 関数はさっきのと同じ // run(): 1 count // run(): 2 count // run(): 3 count // run(): 4 count // run(): 5 count // run(): 6 count // run(): canceled. reasone: context deadline exceeded
パッと見た感じは context.WithTimeout()
と似てる。
実は context.WithTimeout()
は内部で context.WithDeadline()
を呼び出している。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
context がキャンセルしたのをチェックする
Context
構造体の Done()
メソッドが返すチャネルは context がキャンセルするとクローズするので context がキャンセルしたかをチェックできる。
select { // context がキャンセルするとこの case に入る case <-ctx.Done(): // 終了処理 }
context がキャンセルした理由をチェックする
Context
構造体の Err()
メソッドは context がキャンセルした理由を示す error
を返すので context がキャンセルした理由をチェックできる。
たとえば Err()
メソッドは context がキャンセル関数の呼び出しでキャンセルしたら context.Canceled
を返すし、context がタイムアウトしてキャンセルしたら context.DeadlineExceeded
を返す。
Err()
メソッドは context がまだキャンセルしてないときに呼び出すと nil
を返す。
context に値を持たせる
context.WithValue()
関数は値を context に持たせることができ、context が持っている値は Context
構造体の Value()
メソッドで取り出せる。
値を context に持たせるコードはこんな感じ。
type key int const k key = 0 type Values struct { msg string } // 省略 v := Values{msg: "Hello World!"} ctx := context.WithValue(context.Background(), k, v)
context から値を取り出すコードはこんな感じ。
Value()
メソッドは interface{}
型の値を返すので実際に使うときは型アサーションは必須。
v := ctx.Value(k).(Values) fmt.Printf("%s\n", v.msg)
context のタイムアウトまで一定の時間を切ってたらなにもしない
Context
構造体の Deadline()
メソッドは context がタイムアウトする日時の time.Time
を返すので context があとどれくらいでタイムアウトするかチェックできる。
たとえば10秒くらいかかる処理を開始するときに context が5秒後にタイムアウトすることが分かってたら処理を開始しても処理が完了しないのは明らかなので、処理を開始しないでおくみたいなことができる。
Deadline()
メソッドは context がタイムアウトしないときは2番目の返り値は false
を返す。
Deadline()
メソッドを使うコードの例はこんな感じ。run()
関数はタイムアウトまで2秒を切ってたらメッセージを出力して return する。
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() for i := 0; i < 5; i++ { go run(ctx) time.Sleep(1 * time.Second) } } func run(ctx context.Context) { deadline, ok := ctx.Deadline() if !ok { return } now := time.Now() d := deadline.Sub(now).Truncate(time.Second) if d < 2*time.Second { fmt.Printf("run(): do not run! time to deadline: %+v\n", d) return } fmt.Printf("run(): run! time to deadline: %+v\n", d) } // run(): run! time to deadline: 4s // run(): run! time to deadline: 3s // run(): run! time to deadline: 2s // run(): do not run! time to deadline: 1s // run(): do not run! time to deadline: 0s