全力で怠けたい

怠けるために全力を尽くしたいブログ。

いまさら Go の context を少しだけ整理してみたメモ。

はじめに

Go で並行処理を実装するときはたいがい context を使うけど context はすごくよくできててフワッとした理解でもちゃんと使えてしまう。 いまさらだけど自分のなかで context とはなにかどうやって使うものかを少し整理したのでメモしておく。

context とは

context とはなにかだけど Go Concurrency Patterns: Context - The Go Blog の説明が分かりやすかったので引用しておく。

In Go servers, each incoming request is handled in its own goroutine. At Google, we developed a context package that makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request.

(訳) Go のサーバーは受信した各リクエストをそれぞれの goroutine で処理します。 Google はリクエストスコープの値、キャンセルシグナル、デッドラインを API の境界を越えてリクエストの処理に関わるすべての goroutine に簡単に渡すことができる context パッケージを開発しました。

あとこのへん。

A Context is safe for simultaneous use by multiple goroutines. Code can pass a single Context to any number of goroutines and cancel that Context to signal all of them.

(訳) context は複数の goroutine が同時に安全に使える。コードは単一の context を任意の数の goroutine に渡すことができ、その context をキャンセルするとシグナルをすべての goroutine に送ることができる。

context ができること

context は大雑把にはこういうことができる。

  • 値を API の境界を超えて複数の goroutine に渡せる
  • context は木構造にでき、任意の context をキャンセルするとその context とその context の枝に連なるすべての Context をキャンセルできる

context の使い方

context をキャンセルする

キャンセルできる context は context.WithCancel() で作る。

context.WithCancel() の2番目の返り値の関数を呼び出すと1番目の返り値の context をキャンセルできる。

コードはこんな感じ。

package main

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

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go func() { run(ctx) }()

    time.Sleep(5 * time.Second)
    cancel()
    time.Sleep(1 * time.Second)
}

func run(ctx context.Context) {
    i := 1
    for {
        fmt.Printf("run(): %d count\n", i)
        i++
        select {
        case <-time.After(1 * time.Second):
            continue
        case <-ctx.Done():
            fmt.Printf("run(): canceled. reasone: %s\n", ctx.Err().Error())
            return
        }
    }
}

// run(): 1 count
// run(): 2 count
// run(): 3 count
// run(): 4 count
// run(): 5 count
// run(): canceled. reasone: context canceled

main() 関数は ctx, cancel := context.WithCancel(context.Background()) で context を作って run() 関数を新しい goroutine で動かす。

run() 関数は無限ループで1秒ごとに run(): 1 count みたいな文字列を出力する。 run() 関数に渡した context がキャンセルすると ctx.Done() が返すチャネルがクローズするので run() 関数 は return して終了する。

指定した時間が経過したら context をキャンセルする

指定した時間が経過したらキャンセルする context は context.WithTimeout() で作る。

context は context.WithTimeout() の2番目の引数に指定する時間が経過すると自動的にキャンセルする。 context.WithTimeout() の2番目の返り値の関数を呼び出すと context が自動的にキャンセルする前に context をキャンセルできる。

コードはこんな感じ。

package main

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

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    go func() { run(ctx) }()

    time.Sleep(10 * time.Second)
}

// run() 関数はさっきのと同じ

// run(): 1 count
// run(): 2 count
// run(): 3 count
// run(): 4 count
// run(): 5 count
// run(): 6 count
// run(): canceled. reasone: context deadline exceeded

main() 関数は ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) で context を作って run() 関数を新しい goroutine で動かす。

run() 関数は無限ループで1秒ごとに run(): 1 count みたいな文字列を出力する。 run() 関数に渡した context がキャンセルすると ctx.Done() が返すチャネルがクローズするので run() 関数 は return して終了する。

指定した日時になったら context をキャンセルする

指定した日時になったらキャンセルする context は context.WithDeadline() で作る。

context は context.WithDeadline() の2番目の引数に指定する日時になると自動的にキャンセルする。 context.WithDeadline() の2番目の返り値の関数を呼び出すと context が自動的にキャンセルする前に context をキャンセルできる。

コードはこんな感じ。

package main

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

func main() {
    deadline := time.Now().Add(5 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    go func() { run(ctx) }()

    time.Sleep(10 * time.Second)
}

// run() 関数はさっきのと同じ

// run(): 1 count
// run(): 2 count
// run(): 3 count
// run(): 4 count
// run(): 5 count
// run(): 6 count
// run(): canceled. reasone: context deadline exceeded

パッと見た感じは context.WithTimeout() と似てる。 実は context.WithTimeout() は内部で context.WithDeadline() を呼び出している。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

context がキャンセルしたのをチェックする

Context 構造体の Done() メソッドが返すチャネルは context がキャンセルするとクローズするので context がキャンセルしたかをチェックできる。

select {
// context がキャンセルするとこの case に入る
case <-ctx.Done():
    // 終了処理
}

context がキャンセルした理由をチェックする

Context 構造体の Err() メソッドは context がキャンセルした理由を示す error を返すので context がキャンセルした理由をチェックできる。

たとえば Err() メソッドは context がキャンセル関数の呼び出しでキャンセルしたら context.Canceled を返すし、context がタイムアウトしてキャンセルしたら context.DeadlineExceeded を返す。

Err() メソッドは context がまだキャンセルしてないときに呼び出すと nil を返す。

context に値を持たせる

context.WithValue() 関数は値を context に持たせることができ、context が持っている値は Context 構造体の Value() メソッドで取り出せる。

値を context に持たせるコードはこんな感じ。

type key int

const k key = 0

type Values struct {
    msg string
}

// 省略
    v := Values{msg: "Hello World!"}
    ctx := context.WithValue(context.Background(), k, v)

context から値を取り出すコードはこんな感じ。 Value() メソッドは interface{} 型の値を返すので実際に使うときは型アサーションは必須。

   v := ctx.Value(k).(Values)
    fmt.Printf("%s\n", v.msg)

context のタイムアウトまで一定の時間を切ってたらなにもしない

Context 構造体の Deadline() メソッドは context がタイムアウトする日時の time.Time を返すので context があとどれくらいでタイムアウトするかチェックできる。

たとえば10秒くらいかかる処理を開始するときに context が5秒後にタイムアウトすることが分かってたら処理を開始しても処理が完了しないのは明らかなので、処理を開始しないでおくみたいなことができる。

Deadline() メソッドは context がタイムアウトしないときは2番目の返り値は false を返す。

Deadline() メソッドを使うコードの例はこんな感じ。run() 関数はタイムアウトまで2秒を切ってたらメッセージを出力して return する。

package main

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

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    for i := 0; i < 5; i++ {
        go run(ctx)
        time.Sleep(1 * time.Second)
    }
}

func run(ctx context.Context) {
    deadline, ok := ctx.Deadline()
    if !ok {
        return
    }

    now := time.Now()
    d := deadline.Sub(now).Truncate(time.Second)
    if d < 2*time.Second {
        fmt.Printf("run(): do not run! time to deadline: %+v\n", d)
        return
    }

    fmt.Printf("run(): run! time to deadline: %+v\n", d)
}

// run(): run! time to deadline: 4s
// run(): run! time to deadline: 3s
// run(): run! time to deadline: 2s
// run(): do not run! time to deadline: 1s
// run(): do not run! time to deadline: 0s

参考サイト