コードを読んでいると頻繁に遭遇する Context
について整理したいと思います。
(恥ずかしながら今まで雰囲気で読んでいました)
目次
長々書いていますが、次の3つから得られる情報ばかりです。
改訂2版 みんなのGo言語は現場でGo言語使うときに役立つTipsや知っておくと良いことがまとまっており、Go言語による並行処理
は並行処理を書くときのパターンを学ぶことができます。
どちらもGo言語を学ぶ上で役立つことばかりなのでオススメです!!
Contextとは?
なんのためにContextが必要なのか?
Go Blog 「Go Concurrency Patterns: Context」のイントロダクションがわかりやすかったので引用します。
In Go servers, each incoming request is handled in its own goroutine. Request handlers often start additional goroutines to access backends such as databases and RPC services. The set of goroutines working on a request typically needs access to request-specific values such as the identity of the end user, authorization tokens, and the request's deadline. When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using.
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. The package is publicly available as context. This article describes how to use the package and provides a complete working example.
下手くそなりに訳すと、
Goサーバーでは、各リクエストは個別のゴルーチンで処理されます。リクエストハンドラーはDBやRPCなどにアクセスするために追加のゴルーチンを開始することがよくあります。
リクエストに対して動作する一連のゴルーチンは、たいていユーザーIDや認証トークン、リクエストの期限(デッドライン)などのリクエスト固有の値にアクセスする必要があり、リクエストがキャンセル、タイムアウトしたときには、システムがリソースを使えるように、すべてのゴルーチンはすぐに終了すべきです。
Googleでは、リクエストスコープの値やキャンセル通知、API間の期限(デッドライン)を簡単に伝達できる`context`パッケージを開発しました。
要は、
ポイント
Package context のドキュメント Overview を読んで雰囲気を掴む
なんとなく、Contextがある意味がわかったところで、contextパッケージのドキュメント を読んでみます。
Overviewには次のように書かれています。
Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between them must propagate the Context, optionally replacing it with a derived Context created using WithCancel, WithDeadline, WithTimeout, or WithValue. When a Context is canceled, all Contexts derived from it are also canceled.
(省略)
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
func DoSomething(ctx context.Context, arg Arg) error {
// ... use ctx ...
}(省略)
こちらも下手くそなりに訳すと、
Context
型を定義しており、Context
を生成するべきで、サーバーへの発信呼び出し(レスポンス?)はContextを受け入れるべきです。これらの間(リクエスト〜レスポンス発信)の関数呼び出しでは、Contextは伝播されるべきです。このとき WithCancel
、WithDeadline
、WithTimeout
、WithValue
, WithDeadline
, WithTimeout
関数は、親Contextを受け取って子ContextとCancelFunc
を返す。CancelFunc
Contextを構造体の中に格納するべきでない。Contextを必要としている関数に明示的に渡すべきで、慣例的に ctx
という名前で関数の第一引数にする。
要は、
ポイント
Context
- リクエスト着信〜レスポンス発信間の関数呼び出しでは、Contextを伝播していくべき。このとき必要に応じて、
WithCancel
、WithDeadline
、WithTimeout
、WithValue
- 関数の第一引数に
ctx
という名前で渡して伝播する。
【まとめ】Contextとは?
ここまでのまとめ。
【まとめ】Contextとは?
- Contextの伝播は、第一引数に
ctx
という名前で渡す - 必要に応じて
WithCancel
、WithDeadline
、WithTimeout
、WithValue
contextパッケージが提供する型・関数
Contextの概要を掴んだので、更にcontextパッケージのドキュメント を読んでパッケージが提供してくれる型・関数を理解していきます。
type Context interface
まずは Context
インターフェースから。
コードは次のようになっています。(ざっと和訳していますが、一部省略)
type Context interface {
// Deadline は処理をキャンセルする時間を返す。
// 期限を設定していない場合は ok == false を返す。
Deadline() (deadline time.Time, ok bool)
// Done は処理をキャンセルする必要がある場合(Contextがキャンセルされたとき)に閉じられたチャネルを返す。
// Done は Context をキャンセル出来ないときに nil を返す可能性がある。
// Doneチャネルのクローズは、cancel function が実行された後に非同期に起こる可能性がある。
// 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:
// }
// }
// }
Done() <-chan struct{}
// Err は Done がまだ閉じられていない場合 nil を返す。
// Doneが閉じられている場合は Contextがキャンセルされた or 期限を超えた理由がわかるエラーを返す。
Err() error
// Value はこのContextに関連したキーに対応する値を返す。もしキーに対応する値がなければ nil を返す。
// Context values は プロセスとAPI境界を通過するクエストスコープのデータのみに使用すべきで
// オプションのパラメーターを渡すために使用するべきではない。
//
// キーは Context内の値を特定するもので、 Context に値を格納したい関数は
// グローバル変数にキーを割り当て、そのキーは context.WithValue と Context.Value の引数に使う。A key can be any type that supports equality;
// キーには等価比較できる型であれば何でも使える。
Value(key interface{}) interface{}
}
要は、Context
インターフェースには Deadline
, Done
, Err
, Value
の4つの関数があり
ポイント
Deadline
は期限を返すDone
はチャネルを返す。このチャネルはキャンセル・期限切れの場合に閉じられるErr
はDone
チャネルが閉じられた理由を返すValue
はContext
に格納した値を返す。
提供されている関数たち
提供関数
- Background
- WithCancel
- WithDeadline
- WithTimeout
- WithValue
Background
func Background() Context
Contextの生成に使われ、空のContextを返します。
一般的に、メイン関数や初期化などでトップレベルのContextを生成するために使います。
例
ctx := context.Background()
WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
新しい Done
チャネルをもった子Context(親Contextのコピー)と CancelFunc
を返します。この子Contextの Done
チャネルが閉じられるのは、 CancelFunc
が呼び出されたときか、親Contextの Done
チャネルが閉じられたときです。
例
ctx, cancel := context.WithCancel(context.Background())
WithDeadline, WithTimeout
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
d
以内を期限とした子Context(親Contextのコピー)とCancelFunc
を返します。もし親Contextの期限が d
よりも早ければ親Contextと同じになります。
返された子ContextのDone
チャネルが閉じられるのは、「期限を過ぎた場合」、「CancelFunc
が呼び出された場合」、「親Contextの Done
チャネルが閉じられた場合」の3パターン。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout
は WithDeadline
のエイリアスみたいなもので WithDeadline(parent, time.Now().Add(timeout))
と同じ。
例
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
WithValue
func WithValue(parent Context, key, val interface{}) Context
キーに対応する値をもった子Context(親Contextのコピー)を返す。
ctx = context.WithValue(context.Background(), "userID", 1)
Contextを使ったサンプルコード
簡単なサンプルコード
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
// contextを2つ生成
ctx1, cancel1 := context.WithCancel(context.Background())
ctx2, _ := context.WithTimeout(context.Background(), 3*time.Second)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
childProcess(ctx1, "プロセス1")
}()
wg.Add(1)
go func() {
defer wg.Done()
childProcess(ctx2, "プロセス2")
}()
// ctx2は3秒でタイムアウトするので、スリープ中にキャンセルされる
time.Sleep(5 * time.Second)
// 明示的にcancelFuncを呼び出して、ctx1をキャンセルする
cancel1()
wg.Wait()
}
func childProcess(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
// キャンセル時の処理
fmt.Printf("%s canceled. error: %s\n", name, ctx.Err())
return
case <-time.After(1 * time.Second):
fmt.Printf("%s processing...\n", name)
}
}
}
これを実行すると次のようになり、タイムアウトとキャンセルが行われているのがわかります。
$ go run main.go
プロセス1 processing...
プロセス2 processing...
プロセス2 processing...
プロセス1 processing...
プロセス2 canceled. error: context deadline exceeded
プロセス1 processing...
プロセス1 processing...
プロセス1 canceled. error: context canceled
参考
長々書きましたが、次の3つから学ぶことができます。
改訂2版 みんなのGo言語は現場でGo言語使うときに役立つTips集、知っておきたいことがまとまっており、Go言語による並行処理
は並行処理を書くときのパターンを学ぶことができます。
どちらもGo言語を学ぶ上で役立つことばかりなのでオススメです!!