[Golang] コンテキスト

2021-12-13 hit count image

Golangでコンテキスト(Context)とは何者か説明してコンテキストを定義して使う方法について説明します。

概要

今回のブログポストでは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

コンテキストがcanceltimeoutで終了されるとコンテキストの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())が表示されます。その後、WaitGroupDone関数をコールして、サブゴルーチンが終了されたことをメインゴルーチンに通知します。

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でコンテキストを使う方法について見て見ました。WithCancelWithDeadlineWithTimeoutを使ってコンテキストを定義する方法やWithValueを使ってコンテキストでデータを渡す方法、コンテキストをラッピングして使う方法についても見て見ました。コンテキストはゴルーチンを管理する時よく使うのでよく覚えておきましょう。

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

Posts