Goのcontextについて

Go言語

Goの開発をしている時にcontextをよく見かけるけど、contextの用途がよくわからず使用していることがあると思います。
そんな方向けにcontextの機能についてまとめています。

そもそもcontextとは

contextはGo言語で重要な概念の1つであり、並行処理コードの基盤でもあります。contextの役割として以下があります。

  • 他の処理に値を伝搬
  • 処理のキャンセルを通知
  • 処理のデッドライン

以下では各役割について説明をしていきます。

他の処理に値を伝搬

contextはmapのように、キーバリューで値を設定することができます。contextを他の関数やメソッドなどの処理に渡してあげて、各処理でcontextからキーでバリューを取得して処理できます。
例として、ctxにctxKeyをキー、「success propagation」をバリューで設定をしています。そしてctxをprintContextValue関数に渡してあげて、関数の中でctxからctxKeyをキーに値を取得、出力をしています。
ちなみにcontextのキーをkeyとして独自型に定義をした理由として、公開されていない独自型のキーを使うことで、他の処理で値をの上書きをされる危険性を回避するためです。

package main

import (
	"context"
	"log"
)

type key string

const ctxKey key = "cKey"

func main() {
	ctx := context.Background()
	ctx = context.WithValue(ctx, ctxKey, "success propagation")
	printContextValue(ctx)
}

func printContextValue(ctx context.Context) {
	v := ctx.Value(ctxKey)
	log.Println(v)
}

実行結果は以下のように、「success propagation」が出力されていて、無事に値を他の関数に伝搬できています。

% go run main.go
2024/01/17 21:36:26 success propagation

処理のキャンセルを通知

キャンセルする方法として、context.WithCancelを呼び返り値として渡された親のcontextをラップする子供のcontextと, context.CancelFuncというcontextをキャンセルする関数が返ってきます。CancelFuncを実行することで、処理の停止を伝える。子供のcontextのDoneメソッドでキャンセルを受信できる。
例として、キャンセルを受け取った後に「キャンセルされた」を出力するゴルーチンを作成、WaitGroupを使ってゴルーチンの処理が完了するまで待つようにする。
これにより以下の流れの処理になる。

  1. 「開始」が出力
  2. 1秒スリープして、cancel関数が実行
  3. ctx.Done()でキャンセルを受信して、「キャンセルされた」が出力
  4. ゴルーチンの処理が完了したので、「終了」が出力

package main

import (
	"context"
	"log"
	"sync"
	"time"
)

func main() {
	log.Println("開始")
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		defer wg.Done()
		select {
		case <-ctx.Done():
			log.Println("キャンセルされた")
		}
	}()

	time.Sleep(time.Second)
	cancel()
	wg.Wait()
	log.Println("終了")

}

実行結果は以下のように「開始」が出力されてから、1秒待ち、キャンセルが実行され「キャンセルされた」と出力されて、最後に「終了」が出力されている。

% go run main.go 
2024/01/17 21:50:17 開始
2024/01/17 21:50:18 キャンセルされた
2024/01/17 21:50:18 終了

処理のデッドライン

処理をタイムアウトをさせるcontextを生成する方法として、2つの関数があります

  • context.WithTimeout
  • context.WithDeadline

今回はcontext.withTimeoutを紹介します。第一引数にcontext、第二引数にタイムアウトする時間を渡し、返り値としてキャンセルの時と同じようにcontextとキャンセル関数が返される。引数で渡した時間が経過したら処理の停止を送信する。受信はキャンセルと同じくcontextのDoneメソッドを使う。
今回は例として、親(parent)と子(child)の2つのcontextをcontext.WithTimeoutで生成する。それぞれがタイムアウトするのを待つゴルーチンを作成、それぞれのゴルーチンが終了したら完了になる処理になります。

package main

import (
	"context"
	"log"
	"sync"
	"time"
)

func main() {
	log.Println("開始")
	ctx := context.Background()

	parent, cancel1 := context.WithTimeout(ctx, 1*time.Second)
	defer cancel1()
	child, cancel2 := context.WithTimeout(parent, 2*time.Second)
	defer cancel2()

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		select {
		case <-parent.Done():
			log.Println("親完了")
		}
	}()

	go func() {
		defer wg.Done()
		select {
		case <-child.Done():
			log.Println("子完了")
		}
	}()

	wg.Wait()
	log.Println("終了")
}

例の実行結果として、親が終了したタイミングで子も終了されるので、子で2秒のタイムアウトを設定してますが、開始から1秒後に処理が終了します。

% go run main.go
2024/01/17 22:16:20 開始
2024/01/17 22:16:21 親完了
2024/01/17 22:16:21 子完了
2024/01/17 22:16:21 終了

親子のタイムアウトの時間を逆転させると、以下の実行結果のように子供は開始から1秒後で終了しますが、親は2秒後に完了します。

% go run main.go
2024/01/17 22:16:33 開始
2024/01/17 22:16:34 子完了
2024/01/17 22:16:35 親完了
2024/01/17 22:16:35 終了

備考

Goの慣習で関数の最初の引数にcontextを明示的に渡す。contextは通常「ctx」という名前になる。

まとめ

今回はcontextについて解説をしていきました。
contextは並行処理で使われていて、さらに並行処理について知りたい人向けに並行処理の記事を書いていますので、そちらも読んで頂けたらと思います。

【おすすめ記事のリンク】

コメント

タイトルとURLをコピーしました