2018年12月14日金曜日

golang の Context を学ぶ

概要

golang の context パッケージ の基本をサンプルを交えて学びました

環境

  • macOS 10.14.1
  • golang 1.11.2

基本

context.Background() でメインのコンテキストを作成します
そしてそこから以下 4 つのコンテキストのどれかを作成することでタイムアウト処理やキャンセル処理を実装します

  • context.WithCancel(ctx)
  • context.WithTimeout(ctx)
  • context.WithDeadline(ctx)
  • context.WithValue(ctx)

それぞれサンプルを交えて紹介します

WithCancel

cancel() 関数をコールすることで goroutine 側の処理を強制終了させることができます

package main

import (
        "fmt"
        "context"
        "time"
)

func worker(ctx context.Context) {
        for i := 0; i < 10; i++ {
                fmt.Println(i)
                time.Sleep(1 * time.Second)
        }
}

func main() {
        mainctx := context.Background()
        subctx, cancel := context.WithCancel(mainctx)
        go worker(subctx)
        time.Sleep(5 * time.Second)
        cancel()
        select {
        case <-subctx.Done():
                fmt.Println("done")
        }
}

worker 側は 0 から 9 までの数字を 1 秒おきに表示します
それを WithCancel を使うことで 0 から 4 まで表示させて終了させるサンプルです
WithCancel は新たなコンテキストと goroutine 側の処理をキャンセルするための cancel() 関数を返します
cancel() をコールすることで context.Context 型の引数を持つ goroutine 側の処理を強制終了させることができます
また cancel() がコールされると Context.Done() に終了したメッセージを送信します

Context.Done() はチャネルになっています
なので select と組み合わせてメイン側で使うことで goroutine 側の処理を待ちます
チャネルなので cancel() からメッセージが送信されていない状態で Context.Done() を参照しようとすると deadlock エラーになります (default を使ってノンブロッキングにすることもできます)
なので cancel() 関数は必ず呼ばれることが想定されています

今回のサンプルはスリープ -> cancel の流れになっていますが本来はあるアクションやある条件になったら cancel をコールして強制終了させるという使い方になるかなと思います

WithTimeout

指定時間経過したら強制終了させるコンテキストです

package main

import (
        "fmt"
        "context"
        "time"
)

func worker(ctx context.Context) {
        for i := 0; i < 10; i++ {
                fmt.Println(i)
                time.Sleep(1 * time.Second)
        }
}

func main() {
        mainctx := context.Background()
        subctx, cancel := context.WithTimeout(mainctx, 5 * time.Second)
        defer cancel()
        go worker(subctx)
        select {
        case <-subctx.Done():
                fmt.Println("done")
        }
}

サンプルとしては先程の WithCancel と同じ動きになります
こちらは指定時間後にキャンセルさせるためのコンテキストになります
cancel() は defer を使って確実にコールするようにします

いわゆるタイマーのような動作をします
タイマーが 0 になったら終了させたい場合に使えるかなと思います

WithDeadline

指定の時刻に処理を終了させることができます

package main

import (
        "fmt"
        "context"
        "time"
)

func worker(ctx context.Context) {
        for i := 0; i < 10; i++ {
                fmt.Println(i)
                time.Sleep(1 * time.Second)
        }
}

func main() {
        mainctx := context.Background()
        subctx, cancel := context.WithDeadline(mainctx, time.Now().Add(5 * time.Second))
        defer cancel()
        go worker(subctx)
        select {
        case <-subctx.Done():
                fmt.Println("done")
        }
}

先程の WithTimeout は引数に time.Duration を取ります
WithDeadline は時刻を指定することができる time.Time を引数に取ります
サンプルは現在時刻から 5 秒後時間で cancel() するコンテキストを生成しています
時刻を扱う処理になるのでタイムゾーンには注意しましょう

WithValue

key/value として使えるコンテキストです
goroutine の強制終了には使えないので他と組み合わせる必要があります

package main

import (
        "fmt"
        "context"
        "time"
)

func worker(ctx context.Context) {
        for i := 0; i < 10; i++ {
                msg :="%s:%d"
                fmt.Println(fmt.Sprintf(msg, ctx.Value("key"), i))
                time.Sleep(1 * time.Second)
        }
}

func main() {
        mainctx := context.Background()
        subctx := context.WithValue(mainctx, "key", "value")
        subctx2, cancel := context.WithTimeout(mainctx, 5 * time.Second)
        defer cancel()
        go worker(subctx)
        select {
        case <-subctx2.Done():
                fmt.Println("done")
        }
}

コンテキストには伝播という仕組みもあるので goroutine 側で新たにコンテキストを作成して goroutine ないで cancel しても良いと思います

最後に

golang のコンテキストの基本的な使い方を学んでみました
サンプルを動かすことで理解も深まると思います
実際は複数のコンテキストを組み合わせて使ったり伝播したりするのでもっと複雑になると思いますが基本は 4 つなので覚えておきましょう

参考サイト

0 件のコメント:

コメントを投稿