write ahead log

ロールフォワード用

golangでcontextパッケージを使う

使わないから全然覚えられない.

とりあえずサンプルを書いて覚えておく.

詳細や思想はdeeeetさんの記事が非常にわかりやすいのでこれ読めばそれでいいと思う.

大雑把な理解

  1. context.Backgroundでcontextを作るか, よそからもらってきたcontextを使って
  2. With****メソッドを使ってcontextに意味を持たせる
  3. 結果や値はcontextオブジェクトの中に入ってる
メソッド

With****メソッドは以下がある.

メソッド名 用途
WithCancel キャンセル可能にする
WithTimeout 一定時間でキャンセルされるようにする
WithDeadline 指定時間でキャンセルされるようにする
WithValue 値を一緒に引き渡す

どれも新しいcontextオブジェクトを返してくれる.

メンバ

contextのインターフェースは以下のメンバでとてもシンプル. コメントで書いてる意味がホントに正しいかはちょっと怪しいけど.

type Context interface {
    // デッドライン時刻とデッドラインが設定されているかどうか(falseなら未設定)
    Deadline() (deadline time.Time, ok bool)
    // 完了/キャンセルを知らせるチャネル
    Done() <-chan struct{}
    // エラー
    Err() error
    // WithValueで持ち運ぶ値
    Value(key interface{}) interface{}
}

キャンセル可能にする

キャンセル可能にするにはWithCancelを使います.

以下は無限ループで助けを呼び続けるゴルーチンを2秒後にキャンセルする例です.

WithCancelで返される2つ目の関数を呼び出せばキャンセルされるんですね.

package main

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

func infiniteLoop(ctx context.Context) {
    // 終わらないやつ
    for {
        fmt.Println("Help!")
    }
}

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

    go infiniteLoop(ctx)

    // 2秒待ってキャンセルする
    time.Sleep(2 * time.Second)
    cancel()

    select {
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
}

[実行結果]

Help!
Help!
Help!
.
.
.
Help!
Help!
context canceled
Help!
Help!
Help!
Help!

プロセスが死ぬまでゴルーチンが生きてるので, ずっと助けてと叫んでますが....

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

上記の例だとWithTimeoutを使ってもっとシンプルに書くことができます.

package main

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

func infiniteLoop(ctx context.Context) {
    // 終わらないやつ
    for {
        fmt.Println("Help!")
    }
}

func main() {
    ctx := context.Background()

    // 2秒待ってキャンセルする
    ctx, cancel := context.WithTimeout(ctx, 2 * time.Second)
    defer cancel()

    go infiniteLoop(ctx)

    select {
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
}

[実行結果]

Help!
Help!
Help!
Help!
.
.
.
Help!
Help!
Help!
context deadline exceeded
Help!
Help!
Help!
Help!
Help!
Help!
Help!

指定時間になったらキャンセルする

相対的な時刻指定だけではなく, 絶対的な時刻指定も行う事ができます.

package main

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

func infiniteLoop(ctx context.Context) {
    // 終わらないやつ
    for {
        fmt.Println("Help!")
    }
}

func main() {
    ctx := context.Background()

    // 2秒後をデッドラインにする
    ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2 * time.Second))
    defer cancel()

    go infiniteLoop(ctx)

    select {
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
}

[実行結果]

Help!
Help!
Help!
Help!
.
.
.
Help!
Help!
Help!
Help!
context deadline exceeded
Help!

時間設定したけどやっぱり手動でキャンセルする

WithTimeoutやWithDeadlineでキャンセル時刻を指定していても, 戻り値のcancel関数を呼び出せば好きなタイミングでキャンセルを行えます.

package main

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

func infiniteLoop(ctx context.Context) {
    // 終わらないやつ
    for {
        fmt.Println("Help!")
    }
}

func main() {
    ctx := context.Background()

    // 2秒後をデッドラインにする
    ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2 * time.Second))
    defer cancel()

    // やっぱすぐやめた
    cancel()

    go infiniteLoop(ctx)

    select {
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
}

[実行結果]

$ ./context.exe
Help!
context canceled
Help!
Help!
Help!
Help!

キャンセルを伝搬させる

流石にプロセス死ぬまで叫び続けるのはかわいそうなので, キャンセル時には脱出させてあげます.

contextを作成する際にcontextを渡してあげると, Done()を経由してcancelが伝搬していきます.

package main

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

func infiniteLoop(ctx context.Context) {
    innerCtx, cancel := context.WithCancel(ctx)
    defer cancel()
    // 終わらないやつ だったのを終わるようにした
    for {
        fmt.Println("Help!")

        select {
        case <- innerCtx.Done():
            fmt.Println("Exit from hell.")
            return
        }
    }
}

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

    // 無限ループに入る
    go infiniteLoop(ctx)

    // やっぱすぐやめた
    cancel()

    select {
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
    // infiniteLoopのゴルーチンにキャンセルする余裕をあげる
    // これなしだとゴルーチンへ遷移する前にメインルーチンが終了しちゃう様なので
    time.Sleep(1 * time.Second)
}

[実行結果]

$ ./context.exe
Help!
Exit from hell.
context canceled

地獄から出てこれました.よかったですね.

値を引き渡す

contextを使って値を渡していく時にはcontext.WithValueを使います.

値を取得するにはcontext.Valueにキーを渡して取得します.
(interface型が返るので, 型アサーションがいりますね)

package main

import (
    "context"
    "fmt"
)

func main() {
    ctx := context.Background()
    ctx = context.WithValue(ctx, "hoge", 1)

    fmt.Println(ctx.Value("hoge").(int))
}

[実行結果]

$ ./context.exe
1
追記

deeeetさんのeが一つ多かったので訂正しました.(ごめんなさい)

@niconegotoさんありがとうございました.


「キャンセルを伝搬させる」の項のサンプルコードを一部修正しました.
(go 1.10.1で確認)
id:fjwr38 さんありがとうございました.