Go の context


Go を書くときに何となく書いてしまっている context を運用可能に持っていく。

お題

Context を使って、リクエストを投げている時に、一定の時間がたったらグレースフルにリクエストを停止するプログラムを書く。リクエストを処理する関数の方では、Context の Value を取得すること

上記のお題をさっと書けるように、いろいろ調べてみよう。

Context とは

Go Lang のコンテキストは、デッドライン、キャンセレーションシグナル、他のリクエストスコープのバリューを伝播させるものである。 C# とかでいうところの、CancellationToken に近い雰囲気を持っている。これを理解していこう。

Context の生成

コンテキストは、さまざまなAPIで渡す必要があり、よく理解せずに渡しているケースもあるだろう。主に2つのケースがある。


ctx := context.Background()
ctx := context.TDDO()

それぞれ、どう違うだろうか?説明を読むと、どちらも空の、Non-nil のコンテキストなので、違いは無いように思うが、普通は、Background() を使う。なぜなら、TODO()は、どういうコンテキストを使うかわからない場合に使う、つまりその名の通りTODOなのだw

Context をキャンセル可能にする

Context をキャンセル可能にしたい場合は次のように書く。WithCancel の戻りは、コンテキストと、context.CancelFunc つまり関数である。帰ってきた関数を実行すると、コンテキストがキャンセルされる。他にもWithDeadline 等があり、ある時間になったら自動的にキャンセルされるものも書くことができる。

ctx, cancel := context.WithCancel(context.Background())

go func() {
   time.Sleep(5 * time.Second)
   cancel()
}()

Cancel をフックする

キャンセルがかかったとすると、どうやって、そのキャンセルをフックすればいいのだろうか?コンテキストを渡される側の関数のサンプルを書いてみたい。コンテキストのDone() 関数は、コンテキストがキャンセル、もしくは、デッドラインに到達したときに、チャネルの型になっているので、チャネルに送信される。下記のようなプログラムで、他のチャネルに正常終了のチャネルを持たせて、待ち受けるとかすると良さげ。実際のライブラリがどう書かれているのかも読んでパターンを学びたい。

func processRequest(ctx context.Context, uri string) bool {
    finished := make(chan bool)
    go func() {
        // Do something.
        time.Sleep(10 * time.Second)
        finished <- true
    }()
    select {
    case <-ctx.Done():
        fmt.Println("canceled!")
        return true
    case <-finished:
        fmt.Println("successfully done")
        return false
    }
}

Context に値を渡す

他に、context には値を持たすことが出来る。ただ、context.Background は、値を持たせることが出来ないので、次のように書く。

ctx := context.WithValue(context.Background(), "key", "value")

fmt.Printf("Value: %s\n", ctx.Value("key")

実は上記は正しく実行されるが、推奨されない書き方で、GoLint で警告される。本来のおすすめの書き方はこうなる。


type key string
const(
  sessionID key = "sessionID"
)

    :
ctx :context.WithValue(context.Background(), sessionID, "value")
fmt.Printf("Value: %s\n", ctx.Value(sessionID)

なぜこう書かないといけないか?というと、パッケージ感での、key の衝突を避けるためである。GoLintの警告を読むと、KeyはStringなどのプリミティブ型では、衝突が起こるので使うべきでないとある。例えば、"key" というものを使っているとすると、伝播してつかわれる context なので、どこかのパッケージで同じ"key" を使っているかもしれないからである。これを避けるために、type key string を定義することによって、ネームスペースによって明確に別の型として認識されるので、衝突がおこならないかだ。衝突を避けるために、key や、sessionID を小文字スタートにして、private にしている。

Context とツリー構造

では、コンテキストに、キーを渡す場合、キーを渡してかつ、キャンセルしたい場合はどうしたらよいだろうか?

    ancestorCtx := context.WithValue(context.Background(), sessionID, "value1") // Key should be comparable in real world.
    parentCtx := context.WithValue(ancestorCtx, sessionID, "overridden value")  // key should be comparable in real world.
    ctx, cancel := context.WithCancel(parentCtx)
    fmt.Printf("Value : %s\n", ctx.Value(sessionID))

コンテキストの値は、引き継がれるので上記のように書ければよい。ちなみに実行すると、後勝ちで、同じKeyなら上書きされる。

$ ./main 
Value : overridden value

コンテキストは、ツリー状に管理されている。Golang context.WithValue: how to add several key-value pairs
この StackOverflow の解答の一つに書いてあるのだが、コンテキストは、Keyの値をオブジェクトをさかのぼってキーが見つかるまで検索していくためだ。だから、コンテキストは、普通親コンテキストが引数として渡される必要がある。context.Background() は必ず一番の祖先になる。

解答

さて、自分なりの解答を作ってみた。

package main

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

type key string

const (
    sessionID key = "session"
)

func main() {
    // context.TODO()
    ancestorCtx := context.WithValue(context.Background(), sessionID, "value1") // Key should be comparable in real world.
    parentCtx := context.WithValue(ancestorCtx, sessionID, "overridden value")  // key should be comparable in real world.
    ctx, cancel := context.WithCancel(parentCtx)

    go func() {
        time.Sleep(5 * time.Second)
        cancel()
    }()

    processRequest(ctx, "http://localhost:7071/api/hello")
}

func processRequest(ctx context.Context, uri string) bool {
    fmt.Printf("Received context value sessionID: %s\n", ctx.Value(sessionID))
    finished := make(chan bool)
    go func() {
        // Do something.
        time.Sleep(10 * time.Second)
        finished <- true
    }()
    select {
    case <-ctx.Done():
        fmt.Println("canceled!")
        return true
    case <-finished:
        fmt.Println("successfully done")
        return false
    }
}

実行結果

$ ./main
Received context value sessionID: overridden value
canceled!