goのContextの使い方を確認します。
「コンテキストの生成方法」「コンテキストに値を設定する方法」「コンテキストのキャンセル・タイムアウト」などについて取り上げます。
目次
Contextとは
Contextを利用することにより、関数の呼び出し連鎖の中で、以下伝播を行うことができます。
- 値の伝播
- キャンセルの伝播
- タイムアウトの伝播
以下のような用途で活用できます。
- リソースの無駄遣いを防止したとき、
キャンセル・タイムアウトの伝番
を活用。 - APIリクエストに関する情報を各関数で利用したいとき、
値の伝播
を活用。
空のContextの生成
context.Background()
で空のContextを生成できます。
「どのContextを利用すればよいかまだ決まってない場合」「Contextをまだ利用できない場合」には、context.TODO()
の利用が推奨されています。
package main
import (
"context"
"fmt"
)
func main() {
ctx1 := context.Background()
ctx2 := context.TODO()
fmt.Printf("%+v\n", ctx1) // context.Background
fmt.Printf("%+v\n", ctx2) // context.TODO
}
値の伝播
( WithValue, Value )
WithValueメソッド
を利用すると、Key-Value形式で値がセットされた新しいコンテキストを取得できます。
セットした値は Valueメソッド
で取得できます。
package main
import (
"context"
"fmt"
)
const (
key1 = "wakuwaku"
key2 = "bank"
)
func fn1(ctx context.Context) {
ctx = context.WithValue(ctx, key1, "fn1で値をセット")
fmt.Printf("[fn1]\tctx:%v\twakuwaku:%v\tbank:%v\n", &ctx, ctx.Value(key1), ctx.Value(key2))
fn2(ctx)
}
func fn2(ctx context.Context) {
ctx = context.WithValue(ctx, key2, "fn2で値をセット")
fmt.Printf("[fn2]\tctx:%v\twakuwaku:%v\tbank:%v\n", &ctx, ctx.Value(key1), ctx.Value(key2))
}
func fn3(ctx context.Context) {
ctx = context.WithValue(ctx, key1, "fn3で値をセット")
fmt.Printf("[fn3]\tctx:%v\twakuwaku:%v\tbank:%v\n", &ctx, ctx.Value(key1), ctx.Value(key2))
fn4(ctx)
}
func fn4(ctx context.Context) {
ctx = context.WithValue(ctx, key1, "fn4で値をセット")
fmt.Printf("[fn4]\tctx:%v\twakuwaku:%v\tbank:%v\n", &ctx, ctx.Value(key1), ctx.Value(key2))
}
func main() {
ctx := context.Background()
fmt.Printf("[main]\tctx:%v\twakuwaku:%v\tbank:%v\n", &ctx, ctx.Value(key1), ctx.Value(key2))
fn1(ctx)
fn3(ctx)
}
以下の実行結果にて、引数で指定されたコンテキストがベースとなっていることがわかります。
[main] ctx:0x14000104220 wakuwaku:<nil> bank:<nil>
[fn1] ctx:0x14000104230 wakuwaku:fn1で値をセット bank:<nil>
[fn2] ctx:0x14000104240 wakuwaku:fn1で値をセット bank:fn2で値をセット
[fn3] ctx:0x14000104250 wakuwaku:fn3で値をセット bank:<nil>
[fn4] ctx:0x14000104260 wakuwaku:fn4で値をセット bank:<nil>
すでに同じキーで値がセットされていれば、値が上書きされます。
キャンセルの伝播
( WithCancel )
WithCancelメソッド
を利用すると、新しいコンテキストとともに キャンセル関数
を取得できます。
<-ctx.Done()
でキャンセル関数の実行を検知できます。
package main
import (
"context"
"fmt"
"time"
)
func fn1(ctx context.Context) {
log("start fn1")
defer log("done fn1")
for i := 1; i <= 4; i++ {
select {
case <-ctx.Done():
return
default:
log("loop fn1")
time.Sleep(1 * time.Second)
}
}
}
func fn2(ctx context.Context) {
log("start fn2")
defer log("done fn2")
for i := 1; i <= 4; i++ {
select {
case <-ctx.Done():
return
default:
log("loop fn2")
}
}
}
func log(timing string) {
fmt.Printf("%s second:%v\n", timing, time.Now().Second())
}
func main() {
log("start main")
defer log("done main")
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
go fn1(ctx)
go fn2(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(2 * time.Second)
}
fn1関数
のほうはループが全て完了する前にキャンセルが実行されたため、2回しかループ処理が実行されずに終了されました。
start main second:37
start fn2 second:37
loop fn2 second:37
loop fn2 second:37
loop fn2 second:37
loop fn2 second:37
done fn2 second:37
start fn1 second:37
loop fn1 second:37
loop fn1 second:38
done fn1 second:39
done main second:41
タイムアウトの伝播
( WithTimeout )
WithTimeoutメソッド
を利用すると、タイムアウトを設定したコンテキストを取得できます。
以下例では、2秒でタイムアウトするコンテキストを生成しています。
package main
import (
"context"
"fmt"
"time"
)
func fn1(ctx context.Context) {
log("start fn1")
defer log("done fn1")
for i := 1; i <= 4; i++ {
select {
case <-ctx.Done():
return
default:
log("loop fn1")
time.Sleep(1 * time.Second)
}
}
}
func fn2(ctx context.Context) {
log("start fn2")
defer log("done fn2")
for i := 1; i <= 4; i++ {
select {
case <-ctx.Done():
return
default:
log("loop fn2")
}
}
}
func log(timing string) {
fmt.Printf("%s second:%v\n", timing, time.Now().Second())
}
func main() {
log("start main")
defer log("done main")
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
go fn1(ctx)
go fn2(ctx)
time.Sleep(5 * time.Second)
}
fn1関数
のほうはループが全て完了する前にタイムアウトになったため、2回しかループ処理が実行されずに終了されました。
start main second:1
start fn2 second:1
loop fn2 second:1
loop fn2 second:1
loop fn2 second:1
loop fn2 second:1
done fn2 second:1
start fn1 second:1
loop fn1 second:1
loop fn1 second:2
done fn1 second:3
done main second:6