概要
今回のブログポストではGolangでコンテキスト
(Context)を使ってワークフロー(ゴルーチン)を制御する方法について説明します。このブログで紹介するソースコードは下記のリンクで確認できます。
ゴルーチンとチャネルに関して詳しく情報は下記のブログポストを参考してください。
コンテキスト
Golangでコンテキスト(Context)は作業仕様書のような役割をするもので、作業可能時間、作業のキャンセルなどのフローを制御するため使います。
Golangでは次のようにcontext
のパッケージを使ってコンテキストを定義することができます。
import "context"
// Cancel
ctx, cancel := context.WithCancel(context.Background())
// Deadline
ctx, cancel := context.WithDeadline(context.Background(), TIME)
// Timeout
ctx, cancel := context.WithTimeout(context.Background(), DURATION)
WithCancel
コンテキストがcancel
やtimeout
で終了されるとコンテキストのDone
がコールされます。ここではcancel
を使ってコンテキストを終了させる方法について説明します。
ctx, cancel := context.WithCancel(context.Background())
これを確認するためmain.go
ファイルを生成して次のように修正します。
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
wg.Add(1)
ctx, cancel := context.WithCancel(context.Background())
go PrintTick(ctx)
time.Sleep(5 * time.Second)
cancel()
wg.Wait()
}
func PrintTick(ctx context.Context) {
tick := time.Tick(time.Second)
for {
select {
case <-ctx.Done():
fmt.Println("Done:", ctx.Err())
wg.Done()
return
case <-tick:
fmt.Println("tick")
}
}
}
これを実行すると次のような結果が表示されます。
# go run main.go
tick
tick
tick
tick
tick
Done: context canceled
ソースコードを詳しく見るとWaitGroup
を使ってメインゴルーチンがサブゴルーチンを待つようにします。
var wg sync.WaitGroup
func main() {
wg.Add(1)
...
wg.Wait()
}
そしてcontext
パッケージを使ってコンテキストを生成して、これをサブゴルーチンに渡します。
func main() {
...
ctx, cancel := context.WithCancel(context.Background())
go PrintTick(ctx)
...
}
サブゴルーチンを実行されたPrintTick
関数はtime
パッケージのTick
関数を使って毎秒シグナルを発生するチャネルを生成します。また、switch
文を使ってコンテキストのDone
チャネルとtime
パッケージのTick
関数で生成したチャネルからデータを待つようにします。
func PrintTick(ctx context.Context) {
tick := time.Tick(time.Second)
for {
select {
case <-ctx.Done():
fmt.Println("Done:", ctx.Err())
wg.Done()
return
case <-tick:
fmt.Println("tick")
}
}
}
Tick
関数で生成したチャネルでデータが入ってくると、tick
と言う文字列が画面に表示されまし、コンテキストのDone
関数チャネルからデータが入ってくると、Done
文字と終了の理由(ctx.Err()
)が表示されます。その後、WaitGroup
のDone
関数をコールして、サブゴルーチンが終了されたことをメインゴルーチンに通知します。
func main() {
...
ctx, cancel := context.WithCancel(context.Background())
go PrintTick(ctx)
time.Sleep(5 * time.Second)
cancel()
...
}
メインゴルーチンではPrintTick
関数を実行した後、5秒後コンテキストのcancel
をコールしてコンテキストを終了させます。従って、画面にはtick
が5回出力され、コンテキストが終了された理由が画面に表示された後、プログラムが終了されることが確認できます。
WithDeadline
コンテキストのDeadline
はワークフロー(ゴルーチン)をいつまで維持するかを決めるとき使います。
ctx, cancel := context.WithDeadline(context.Background(), TIME)
これを確認するためmain.go
ファイルを次のように修正します。
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
wg.Add(1)
d := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
go PrintTick(ctx)
time.Sleep(time.Second * 5)
cancel()
wg.Wait()
}
func PrintTick(ctx context.Context) {
tick := time.Tick(time.Second)
for {
select {
case <-ctx.Done():
fmt.Println("Done:", ctx.Err())
wg.Done()
return
case <-tick:
fmt.Println("tick")
}
}
}
これを実行すると次のような結果が表示されます。
# go run main.go
tick
tick
tick
Done: context deadline exceeded
cancel
を説明する時使った例題から、コンテキストを生成する時、今から3秒後コンテキストを終了させるデッドラインを指定しています。
d := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
従って、tick
が3回画面に表示された後、デッドラインでコンテキストが終了されることが確認できます。しかし、メインゴルーチンは5秒間維持されるので、5秒後にプログラムが終了されることが確認できます。
func main() {
...
d := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
...
time.Sleep(time.Second * 5)
cancel()
...
}
デッドラインを指定してコンテキストを終了させても、cancel
関数を使ってコンテキストをクルーズするコードを追加する必要があります。
WithTimeout
コンテキストのTimeout
はワークフロー(ゴルーチン)をどのぐらい維持するかを決めるとき使います。
ctx, cancel := context.WithTimeout(context.Background(), TIME)
これを確認するためmain.go
ファイルを次のように修正します。
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
wg.Add(1)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
go PrintTick(ctx)
time.Sleep(time.Second * 5)
cancel()
wg.Wait()
}
func PrintTick(ctx context.Context) {
tick := time.Tick(time.Second)
for {
select {
case <-ctx.Done():
fmt.Println("Done:", ctx.Err())
wg.Done()
return
case <-tick:
fmt.Println("tick")
}
}
}
これを実行すると次のような結果が表示されます。
# go run main.go
tick
tick
tick
Done: context deadline exceeded
結果はDeadline
を使った時と同じことが確認できます。2つの違さはDeadline
はいつまで維持するかを決めることで、Timeout
はどのぐらい維持するかを決めることです。
// Deadline
d := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
タイムアウトもタイムアウトでコンテキストを終了させても、cancel
関数を使ってコンテキストをクローズするコードを追加する必要があります。
WithValue
コンテキストのWithValue
を使ってチャネルようにサブゴルーチンにデータを渡すことができます。
ctx := context.WithValue(context.Background(), KEY, VALUE)
v := ctx.Value(KEY)
これを確認するためmain.go
ファイルを次のように修正します。
package main
import (
"context"
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(1)
ctx := context.WithValue(context.Background(), "v", 3)
go square(ctx)
wg.Wait()
}
func square(ctx context.Context) {
if v := ctx.Value("v"); v != nil {
n := v.(int)
fmt.Println("Square:", n*n)
}
wg.Done()
}
これを実行すると次のような結果が表示されます。
# go run main.go
Square: 9
コンテキストラッピング
Golangではコンテキストを次のようにラッピング(Wrapping)して使うことができます。
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, "key", "value")
ctx = context.WithValue(ctx, "key2", "value2")
これを確認するためmain.go
ファイルを次のように修正します。
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
wg.Add(1)
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, "s", 2)
go PrintTick(ctx)
time.Sleep(5 * time.Second)
cancel()
wg.Wait()
}
func PrintTick(ctx context.Context) {
tick := time.Tick(time.Second)
if v := ctx.Value("s"); v != nil {
s := v.(int)
tick = time.Tick(time.Duration(s) * time.Second)
}
for {
select {
case <-ctx.Done():
fmt.Println("Done:", ctx.Err())
wg.Done()
return
case <-tick:
fmt.Println("tick")
}
}
}
これを実行すると次のような結果が表示されます。
# go run main.go
tick
tick
Done: context canceled
WithCancel
の例題からWithValue
を使ってコンテキストをラッピングしました。
func main() {
...
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, "s", 2)
...
}
このようにコンテキストをラッピングしてコンテキストを使って値を渡したら、渡して貰った値を使ってtick
文字列を1秒ではなく2秒で1回表示されます。
func PrintTick(ctx context.Context) {
tick := time.Tick(time.Second)
if v := ctx.Value("s"); v != nil {
s := v.(int)
tick = time.Tick(time.Duration(s) * time.Second)
}
...
}
このようにコンテキストは複数回ラッピングして作業仕様書を作成することができます。
完了
これでGolangでコンテキストを使う方法について見て見ました。WithCancel
、WithDeadline
、WithTimeout
を使ってコンテキストを定義する方法やWithValue
を使ってコンテキストでデータを渡す方法、コンテキストをラッピングして使う方法についても見て見ました。コンテキストはゴルーチンを管理する時よく使うのでよく覚えておきましょう。
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Deku
が開発したアプリを使ってみてください。Deku
が開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。