概要
Go言語ではゴルーチンによりコードを並行に実行することができ、その同期処理をchannelでハンドリングできます。
channelは値の読み書き、キャンセルをすることができますが、タイムアウトやエラーが起こったときのハンドリングするには独自に実装する必要があります。
contextパッケージを使うことでキャンセルの理由や関数処理を終わらせるデッドラインなどの追加情報も伝達できます。
この記事ではcontextパッケージについて解説します。
開発環境
$ go version
go version go1.10 darwin/amd64
はじめのサンプルコード
まず、動くサンプルコードをみてみます。チャネルを使ったコードとContextを使ったコードです。
「Go言語による並行処理」にあったものをサンプルコードを私のGitHubに載せています。
チャネルのみを使う
サンプルコードはこちら
contextを使う
サンプルコードはこちら
contextパッケージの定義
公式ドキュメントからcontextの定義をみてみましょう。
Variables
var Canceled = errors.New("context canceled")
Canceled
はContext.Err
から返されるエラーであり、contextがキャンセルされた時に呼ばれます。
var DeadlineExceeded error = deadlineExceededError{}
DeadlineExceeded
は、コンテキストの期限が過ぎたときにContext.Err
によって返されるエラーです。
func WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
返されたcancel関数が呼ばれた時にそのdoneチャネルを閉じる新しいContextを返します。
返されたコンテキストのDoneチャネルは、返されたcancel関数が呼び出されたとき、または親コンテキストのDoneチャネルが閉じられたときのどちらか早いほうで閉じられます。
このコンテキストをキャンセルすると、それに関連するリソースが解放されるため、このコンテキストで実行されている操作が完了するとすぐにコードはcancel関数を呼び出す必要があります。
func WithDeadline
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
マシンの時計が与えられたdeadlineの時刻を経過したらそのdoneチャネルを閉じる新しいContextを返します。
期限がdeadlineより遅くならないように調整された親コンテキストのコピーを返します。親の期限がすでにdeadlineよりも早い場合は意味的には親コンテキストと同じものを返します。。
返却されたコンテキストのDoneチャネルは、期限が切れる、返却されたcancel関数が呼ばれた、または親コンテキストのDoneチャネルが閉じられたどちらか早い方で閉じられます。
このコンテキストをキャンセルすると、それに関連するリソースが解放されるため、このコンテキストで実行されている操作が完了するとすぐにコードはcancelを呼び出す必要があります。
func WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
与えられたtimeoutだけ経過したらそのdoneチャネルを閉じる新しいContextを返します。
具体的にはWithDeadline(parent, time.Now().Add(timeout))
を返します。
このコンテキストをキャンセルするとそれに関連するリソースが解放されるので、このコンテキストで実行されている操作が完了するとすぐにコードはcancelを呼び出す必要があります。
サンプルコード
この例では、タイムアウト付きのコンテキストを渡して、タイムアウトが経過した後にコンテキストをその機能を放棄するようにブロッキング関数に伝えます。
code
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
output
context deadline exceeded
type CancelFunc
type CancelFunc = context.CancelFunc
CancelFuncは、その作業を中止するように操作に指示します。 CancelFuncは作業が停止するのを待ちません。最初の呼び出しの後、CancelFuncへの後続の呼び出しは何もしません。
type Context
type Context = context.Context
Contextは、期限、キャンセルシグナル、およびAPIの境界を越えてその他の値を伝達します。 コンテキストのメソッドは、複数のゴルーチンによって同時に呼び出されることがあります。
func Background
func Background() Context
Backgroundはnil以外の空のContextを返します。キャンセルされることはなく、値もなく、期限もありません。通常、main関数、初期化、テストによって使用され、着信要求の最上位コンテキストとして使用されます。
Contextを始める際に使用するメソッドです。
func TODO
func TODO() Context
TODOはnil以外の空のContextを返します。
本番環境で使うことをは想定していないが、どのContextを使ったらいいかが不明なとき、上流のコードの実装が終わっていないが何かしらのContextが来ることはわかっているときのプレースホルダーを提供する。
TODOは、コンテキストがプログラム内で正しく伝播されるかどうかを判断する静的分析ツールによって認識されます。
func WithValue
func WithValue(parent Context, key interface{}, val interface{}) Context
WithValueは、keyに関連付けられた値がvalである新しいContextを返します。
コンテキスト値はプロセスやAPIの境界を通過するリクエストスコープでのデータに絞って使いましょう。
関数にオプションのパラメーターを渡すために使うべきではありません。
WithValueメソッドを使う際に必要なことは次の2つです。
- 使用するキーはGoでの比較可能性を満たさなければなりません。つまり、等値演算子の==と!=を使った時に正しい値を返す必要があります。
- 返された値は複数のゴルーチンからアクセスされても安全でなければなりません。
「Go言語による並行処理」から
WithValueでデータを保管するサンプル
値に型安全がない(interface{})サンプル
https://github.com/SatoTakeshiX/sample-go-context/blob/master/useValueWithInterface/contextValueInterface.go
値に型を定義したサンプル
Contextで保存するべきデータ
公式ドキュメント
コンテキスト値はプロセスやAPIの境界を通過するリクエストスコープでのデータに絞って使いましょう。
関数にオプションのパラメーターを渡すために使うべきではありません。
「Go言語による並行処理」で言及されていた「リクエストスコープでのデータ」
(これを必ず守らなければ行けないものではないが、定義の指針となる作者の経験則)
- データはプロセスやAPIの境界を通過すべき
- データは不変であるべき
- データは単純な型に向かっていくべき
- データはデータであるべきでメソッド付きの型であるべきではない
- データは修飾の操作を助けるべきものであって、それを駆動するものではない
「Go言語による並行処理」によるAPIの各データと5つの経験則を満たすかどうか
データ | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
リクエストID | ✓ | ✓ | ✓ | ✓ | ✓ |
ユーザーID | ✓ | ✓ | ✓ | ✓ | |
URL | ✓ | ✓ | |||
APIサーバーの接続 | |||||
認可トークン | ✓ | ✓ | ✓ | ✓ | |
リクエストトークン | ✓ | ✓ | ✓ |
まとめ
公式ドキュメントと「Go言語による並行処理」を並べながらGoにおけるcontextパッケージの使い方を解説しました。
GoでAPIなどを作る上で役立つパッケージです。
ぜひ使いこなしていきましょう!