Contextの使い方(Background, WithValue, WithCancel, WithTimeout)

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

参考

よかったらシェアしてね!
目次