それが僕には楽しかったんです。

ミドルウェアとかやってます

GolangのContextを頑張って理解していく

はじめに

どうも最近吹き替えが酷い映画とドウェイン・ジョンソン主演の大体の映画にハマっているけんつです。

Golang書いていてちょくちょくでてくる context.Background() って何やっているのと思ったのでちょいと勉強がてらまとめていこうかと思います。

Contextパッケージ

今回の主な話としてはContextパッケージの話とそのContextとは何かというところになる。
ContextパッケージはGolangに標準で組み込まれているパッケージで、このパッケージに含まれるContextなる型はプロセスをまたいでデッドラインやキャンセルシグナルを伝達することができる機能群を提供している。
オライリーから出版されている「GO言語による並行処理」を参考にこれらを簡潔に説明するのならば、Context型は並行処理内でアクションの通知以外にもキャンセルした理由やデッドラインの有無などという状態を付加するために使用される。

golang.org

詳しい説明は公式ドキュメントに記載されているので気になる場合は確認して欲しい。
次からはよく使う関数やメソッド群を実例を踏まえてまとめていく。

実例

今回使用したコード↓

package main

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

func main() {

    // 空のContextを生成
    ctx := context.Background()
    go parentRoutine(ctx)

    time.Sleep(20 * time.Second)

    fmt.Println("main func finish")

}

func parentRoutine(ctx context.Context) {

    subCtx, cancel := context.WithCancel(ctx)
    subsubCtx, cancel2 := context.WithCancel(subCtx)

    go childRoutine(subCtx, "sub context")
    go childRoutine(subsubCtx, "sub sub context")

    time.Sleep(5 * time.Second)

    cancel2()

    time.Sleep(5 * time.Second)

    cancel()

    tCtx, _ := context.WithTimeout(ctx, time.Second * 5)
    go childRoutine(tCtx, "context with timeout")

}

func childRoutine(ctx context.Context, prefix string) {
    for i := 0; ; i++{

        select {

        case <-ctx.Done():
            fmt.Printf("routine %s cancel.\n", prefix)
            return

         case <-time.After(1 * time.Second):
             fmt.Printf("routine %s has value %d \n", prefix, i)

        }
    }
}

実行結果↓

$ go run context.go 
routine sub sub context has value 0 
routine sub context has value 0 
routine sub sub context has value 1 
routine sub context has value 1 
routine sub sub context has value 2 
routine sub context has value 2 
routine sub sub context has value 3 
routine sub context has value 3 
routine sub sub context cancel.
routine sub context has value 4 
routine sub context has value 5 
routine sub context has value 6 
routine sub context has value 7 
routine sub context has value 8 
routine sub context cancel.
routine context with timeout has value 0 
routine context with timeout has value 1 
routine context with timeout has value 2 
routine context with timeout has value 3 
routine context with timeout cancel.
main func finish

解説

コンテキスト生成

まずはコンテキスト生成に着目していく。

ctx := context.Background()

コンテキスト生成には今回上記の関数を使用している。
これには context.TODO() も使用することができる。BackGround() はそれぞれ nil 以外の空のコンテキストを返し、これは値もデッドラインも存在しない最上位のコンテキストとして存在する。
TODO() はnil 以外の空のコンテキストを返すという部分は同じだがどのコンテキストを使用するか不明な場合やまだ使用できない?場合はこちらを使用するべきらしい。今のところはTODOで生成されたコードを見たことがないのでよくわからない。

もうひとつの方法として context.WithValue(...) を使用することもできる。この場合はそのコンテキストに値を渡すことが可能になる。

subCtx, cancel := context.WithCancel(ctx)
subsubCtx, cancel2 := context.WithCancel(subCtx)

次に、この部分に注目して欲しい。この部分ではコンテキストを生成しているが明示的にキャンセルすることが可能な状態で生成している。
そして subCtx から subsubCtx を生成していることにもこの時注目して欲しい。ちなみに cancel は _ で省略することもできる。

キャンセルシグナル

WIthCancel でコンテキストを生成した場合、第2変数で明示的にそのgoroutineを終了させることができる。そのように終了した場合 ctx.Done() がコールされる。
最初に cancel2() をコールして subsubCtx 側をキャンセルしているがこれの順序を逆にすると subCtx から生成された subsubCtx も同時にキャンセルされることとなる。

そして WithTImeout の場合は明確で、設定した時間だけ時間が経過すると自動的にキャンセルされる。

おわりに

Contextとは 最初にも記述したが並行処理においてそのアクションをした通知だけでなく何故そのアクションをしたかなどの状態をもつ伝播するものだということがわかった。
またそれには値を伝播させることができるため、同一のリクエストスコープで使用することが推奨される。つまり、構造体やグローバルな変数として扱っては行けないということだ。
今回はContextの割と基本的な部分についてまとめたが goroutineリークなどと絡めてまたまとめることがあれば続きを書く。かも。