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

contextの使い方

More than 3 years have passed since last update.

https://tip.golang.org/doc/go1.7#context

Go1.7で golang.org/x/net/context が
標準ライブラリに入るようなので改めて使い方のメモ。

変数の管理

ctx := context.Background()
ctx = context.WithValue(ctx, "Foo", 1)
ctx = context.WithValue(ctx, "Bar", 2)

fmt.Println(ctx.Value("Foo").(int)) // 1

よくあるやつ。値を代入するたびに新しいcontextが生成される。
なので、乱暴に値を上書かない限りはあまり状態を気にしなくていい。

TimeoutとCancel

func WithCancel func WithDeadline func WithTimeout はキャンセルを取り扱う。

WithTimeout, WithDeadline は特定時間になったタイミングでキャンセルされる。
引数で指定の仕方が 何秒後かと時刻指定がなのが違うだけ。

実際にWithTimeoutの中身は WithDeadlineを呼んでいるだけ。

WithTimeout, WithDeadline

指定した時間後にキャンセルイベントが起きる。

// 5秒かかる処理
func heavyFunc(ctx context.Context) {
  time.Sleep(5 * time.Second)
}

func main() {
  ctx := context.Background()
  ctx, cancel := context.WithTimeout(ctx, time.Second) // 1秒後にキャンセル
  defer cancel()

  go heavyFunc(ctx)
  go heavyFunc(ctx)

  select {
  case <-ctx.Done(): // 1秒以上かかると Done() がcloseされる
    fmt.Println("done:", ctx.Err()) // done: context deadline exceeded
  }
}

WithCancel

自分でキャンセルを使う場合に使う

func heavyFunc(ctx context.Context) {
  time.Sleep(5 * time.Second)
}

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

  go heavyFunc(ctx)
  go heavyFunc(ctx)

  cancel() // 即キャンセル

  select {
  case <-ctx.Done():
    fmt.Println("done:", ctx.Err()) // done: context canceled
  }
}

キャンセルの伝搬

親がキャンセルされると子にキャンセルが伝搬されていく

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

  parent, parentCancel := context.WithCancel(ctx)
  child, childCancel := context.WithCancel(parent)
  defer childCancel()

  go heavyFunc(ctx)

  go func() { // 親を1秒後にキャンセル
    time.Sleep(time.Second)
    parentCancel()
    fmt.Println("parent cancelled")
  }()

  select {
  case <-child.Done(): // 子もキャンセルされる
    fmt.Println("parent:", parent.Err()) // parent: context canceled
    fmt.Println("child:", child.Err()) // child: context canceled
  }
}

使い方

goroutineで処理を実行して、所定より時間がかかった場合にエラーを返すのに使う。
ただ、結果が欲しい場合は、contextとは違う形でchannelを使い、値を受け取って返すようにする。(だいたいこっちの使い方になる)

func heavyFunc(ctx context.Context) error {
  time.Sleep(time.Second)
  return nil
}

func exec(ctx context.Context) error {
  errChan := make(chan error, 1)

  go func() {
    errChan <- heavyFunc(ctx)
  }()

  select {
  case <-ctx.Done(): // キャンセルが発生した場合
    return ctx.Err()
  case err := <-errChan: // heavyFuncの結果を取得した場合
    return err
  }
}

func main() {
  ctx := context.Background()
  ctx, cancel := context.WithTimeout(ctx, 2*time.Second) // 全体2秒でタイムアウト
  defer cancel()

  if err := exec(ctx); err != nil {
    log.Fatal(err)
  } else {
    log.Print("ok")
  }
}

まとめ

contextを使うと、値の取り回しやキャンセル処理は楽になる。だけど、goroutineを大量に使って細かく状態を管理をやってくれるものではないので注意。

参考

x/net/context の実装パターン

Why do not you register as a user and use Qiita more conveniently?
  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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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