Help us understand the problem. What is going on with this article?

Go の Context を学ぶ

Azure SDK for go を使っていると、突然 Context というものが出てきて意味がわからなかったので調べて見た。

公式のリファレンスを読んだのちに

次のブログがとてもわかりやすかった。

ざっとまとめると、Context は、APIのサーバーやクライアントを使うときに、コンテキストを提供してキャンセルや、タイムアウト、値を渡したり出来る仕組み。

Context 構造体の持っている属性や関数に関しては上記のブログに書いてあるので、ここでは書かないけど、何もなし(Background) キャンセル、タイムアウト、値渡しなどの関数が存在する。Context の構造体自体はキャンセルのメソッドは提供しないが、Context はデコレーターの様になっておりラップが可能になっている。上記のブログのサンプルを拡張して見た。

基本的な使い方

go routine などを呼び出す元の方でオブジェクトを生成する。context.Background() は空のコンテキストを生成する。

ctx := context.Background()

これをgo routine を実行時に引き渡す。

    go infiniteLoop(ctx)

go routine の方では、それを受け取る。この例ではループが回るが、キャンセルされたら、Done() が呼ばれるので、メッセージを出力する。

ポイントは、コンテキストオブジェクトは、デコレータの様な使い方をするところ。この例では、渡ってきたのは、空のContextが渡ってきているが、ここで、context.WithCancel(ctx) の様にラップしてキャンセル機能を追加している。

func infiniteLoop(ctx context.Context) {
    innerCtx, cancel := context.WithCancel(ctx)
    defer cancel()
    for {
        fmt.Println("Waiting for time out")

        select {
        case <-innerCtx.Done():
            fmt.Println("Exit now!")
            return
        default:

        }
    }
}

タイムアウトの実装

たとえばタイムアウトを追加したければ、本体側でこんな感じで書けば良い。キャンセルされたらキャンセルの関数が呼ばれる様に defer 関数を使っておく。

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

しっかりと 5秒後にキャンセルされ、Done が呼ばれる。

Key/Value 値を設定する

Context はキーバリューも保持できる様になっている。これもデコレータ的な使い方をする。

    ctx := context.Background()
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    ctx = context.WithValue(ctx, "message", "hi")

go routine 側でこの値を取りたければこんな感じにすると良い。KeyValue の型はinterface {} なので、型変換が必要。

fmt.Println("message:", ctx.Value("message").(string))

コード全体

こちらにコードを置いておいた

全体を載せておく。

package main

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

func infiniteLoop(ctx context.Context) {
    innerCtx, cancel := context.WithCancel(ctx)
    defer cancel()
    for {
        fmt.Println("Waiting for time out")
        //time.Sleep(time.Second)
        // If I put sleep in here, fmt.Println doesn't output the "Exit now!"

        select {
        case <-innerCtx.Done():
            fmt.Println("Exit now!")
            fmt.Println("message:", ctx.Value("message").(string))
            return
        default:

        }
    }
}

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

    go infiniteLoop(ctx)

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

次のステップ

Context は理解できたと思うが、上記のサンプルコードには一点わからないことがある。上記のもので、time.Sleep(time.Second) を有効にすると、キャンセル時に、fmt.Println("Exit now!") が実行されない。なんでだろう。

TsuyoshiUshio@github
プログラマ。自分の学習用のブログです。内容は会社とは一切関係ありません。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away