技術向上

プログラミングの学び、気になるテクノロジーやビジネストレンドを発信

context【Go】

処理のキャンセル

時間がかかる処理を持つgoroutine関数に対して
時間制限を持たせる場合などに引数として渡します。

func longProcess( ctx context.Context, c chan string) {    // 明示的にすべきとの観点から、contextは第1引数に渡すべきである、とgodocにも記載されている。
    fmt.Println("run")
    time.Sleep(time.Second * 2)    // 2秒間待つ
    fmt.Println("finish")
    c <- "result"
}

func main() {
    c := make(chan string)
    ctx := context.Background()    // contextを作成
    ctx, cancel := context.WithTimeout(ctx, 1*time.Second)    // 渡し先の処理開始から1秒間でタイムアウトさせる
    defer cancel()
    go longProcess(ctx, c)    // contextを渡す
    
loop:
    for {
        select {
        case <-c:
            fmt.Println("success")
            break loop
        case <-ctx.Done():    // contextが終了した場合
            fmt.Println(ctx.Err())    // contextが終了した理由を出力
            break loop
        }
    }
    fmt.Println("##################")
}


context.WithTimeout()は、引数に生成したcontextと、timeoutに至るまでの時間を指定します。
ここで言う時間は、渡す先のgoroutine関数の処理が開始されてからの時間です。
時間に依存せず、何らかの条件分岐を使って処理を終了させたい場合には、
context.WithTimeout()の2番目の返り値である、cancel()を実行します。
(cancelは変数に格納するので、名前は任意です。)

cancelまでの時間を指定する代わりに、時刻を指定することもできます。

ctx := context.Background()
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(1*time.Second))    // 現在時刻から1秒後にタイムアウト
defer cancel()


また、context.WithTimeout()を利用する場合、あらかじめどういう時に処理を終了させるかの条件が決まっているかと思います。
その条件が決まっていない時には、context.TODO()を用いることで、とりあえず関数やメソッドに引数を渡す、ということができます。

func main() {
    c := make(chan string)
    ctx := context.TODO()
    go longProcess(c, ctx)
    ...
}

たとえ渡し先の関数やメソッドが、nil値を許容していても、nil値を渡すことは推奨されていません。 context.TODO()を使います。
ただし、後ほど紹介する、値の引き渡しも利用しない場合です。

なお、同じcontextを複数の異なるgoroutineに渡しても問題ありません。


リクエストスコープにおける値の引き継ぎ

リクエスト毎に値を保持するユーザーIDなどの引き継ぎに使用します。

//値の設定
func WithValue(parent Context, key, val interface{}) Context
//値の取得
func Value(key interface{}) interface{}

keyに指定する型は、他packageとの競合を避けるため、
stringなどの標準型ではなく、type定義した型である必要があります。
また、Valueの返り値の型はinterface{}であるため、type assertionが必要です。
Valueは、値が存在するかをbool値で返すことも可能です。

以下に簡単な使用例を示します。
user_idをmain関数でセットして、goroutineで出力する、というものです。

func longProcess(ctx context.Context, c chan string) {
    fmt.Println("run")
    time.Sleep(time.Second * 2)
    v, ok := GetValueS(ctx, key("user_id"))    // bool値も返す
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("I couldn't find user_id")
    }
    fmt.Println("finish")
    c <- "result"
}

type key string

func SetValueS(ctx context.Context, key interface{}, value string) context.Context {
    return context.WithValue(ctx, key, value)
}

func GetValueS(ctx context.Context, key interface{}) (string, bool) {
    v, ok := ctx.Value(key).(string)    // interface{}を返すため、type asertionが必要
    return v, ok
}

func main() {
    c := make(chan string)
    ctx := context.Background()
    ctx = SetValueS(ctx, key("user_id"), "xch-A1281")
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    go longProcess(ctx, c)

loop:
    for {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Err())
            break loop
        case <-c:
            fmt.Println("success")
            break loop
        }
    }
    fmt.Println("##################")
}


値の引き渡しには以下の慣習があります。

  • 渡し先が状態の変化を把握できないため、状態を持たない値に使用すること
  • API通信やプロセス間通信におけるリクエストスコープの値継承に使用し、通常の関数には用いないこと

一見便利な機能ですが、interface{}を引数とすることから、型安全でなくなる点があり、 通常の関数には用いないなど、利用には慎重になる必要があります。

christina04.hatenablog.com

medium.com