技術メモ

技術メモ

ラフなメモ

GolangのContext

背景など

https://blog.golang.org/context

Goを使ったサーバでは、それぞれのリクエストはgoroutineを用いて扱われる。リクエストハンドラーは別のバックエンドのデータベースやRPCサーバにアクセスするために別のgoroutineを生成して処理する。これらのgoroutineでは値や認証情報、トークン、デッドラインなどリクエスト固有の値にアクセスする必要がある。

リクエストがキャンセルされたときに、すべてのgoroutineはリソースを開放するために速やかに処理を中断すべきである。 contextはAPIの境界を超えて、リクエストの処理に関係する値やデッドラインをすべてのgoroutineに簡単に渡せることができる。

Context のインターフェースは以下のメソッドで構成される。

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

Contextはゴルーチンセーフで、コードで任意の数のゴルーチンにContextを渡すことができる。Contextの親は子をキャンセルすることができるが、子は親をキャンセルできない。子の操作は親の操作をキャンセルできるべきではない。 DoneメソッドはContextの裏で動くすべての関数にキャンセルのシグナルを送るチャネルを返す。 ErrメソッドはなぜそのContextがキャンセルされたかを示すエラーを返す。 Deadlineメソッドは関数が動作し始めるかどうか決めるためのメソッド。 ValueメソッドによってContext固有のデータを伝播することができる。

具体例

今、以下のような一定の時間がかかる処理をするプログラムを考える。

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func work() error {
    defer wg.Done()

    for i := 0; i < 5; i++ {
        select {
        case <-time.After(2 * time.Second):
            fmt.Printf("Done work (%v) !\n", i)
        }
    }
    return nil
}

func main() {
    fmt.Println("Start work...")
    wg.Add(1)
    go work()
    wg.Wait()
    fmt.Println("Finish work...")
}
Start work...
Done work (0) !
Done work (1) !
Done work (2) !
Done work (3) !
Done work (4) !
Finish work...

このプログラムにタイムアウトを付与することにする。バッファ付きチャネルを用いると以下のように実装できる。

package main

import (
    "fmt"
    "log"
    "sync"
    "time"
)

var wg sync.WaitGroup

func work() error {
    defer wg.Done()

    for i := 0; i < 5; i++ {
        select {
        case <-time.After(2 * time.Second):
            fmt.Printf("Done work (%v) !\n", i)
        }
    }

    return nil
}

func main() {
    fmt.Println("Start work...")

    ch := make(chan error, 1)
    go func() {
        ch <- work()
    }()

    select {
    case err := <-ch:
        if err != nil {
            log.Fatal(err)
        }
    case <-time.After(3 * time.Second):
        fmt.Println("process timeout...")
    }

    fmt.Println("Finish work...")
}
Start work...
Done work (0) !
process timeout...
Finish work...

処理はキャンセルできるようになった。しかしこのときの問題は、goroutineで処理されているプログラムはまだ動いていて、リソースを消費していることだ。goroutineをキャンセルする必要がある。

Contextを用いてgoroutineをキャンセルすることができる。

package main

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

var wg sync.WaitGroup

func work(ctx context.Context) error {
    defer wg.Done()

    for i := 0; i < 5; i++ {
        select {
        case <-time.After(2 * time.Second):
            fmt.Printf("Done work (%v) !\n", i)
        case <-ctx.Done():
            fmt.Printf("Cancel the context (%v) .\n", i)
            return ctx.Err()
        }
    }

    return nil
}

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

    fmt.Println("Start work...")

    wg.Add(1)
    go work(ctx)
    wg.Wait()

    fmt.Println("Finish work...")
}
Start work...
Done work (0) !
Cancel the context (1) .
Finish work...

これでContextによってgoroutineをキャンセルできるようになった。

Contextのルール

Package context のOverviewの内容の一部である。

  • struct の一部に含めてはいけない。以下のようにContextは関数の第一引数にすべき
func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}
  • 関数の呼び出しもとのcontextを呼び出し先の関数に伝播させる
  • 適切にキャンセル処理を実装する
  • nil Contextを渡さない。使う場合は context.TODO を用いる

参考