LoginSignup
4
1

More than 3 years have passed since last update.

GoのContextパッケージについて

Posted at

Contextとは

  • タイムアウト処理やgoroutineの処理停止を統一的に扱うためのパッケージ
  • 1.7から標準パッケージとして導入された
  • 提供されている機能としては以下の2つ
    • コールグラフの各枝をキャンセルするAPI
      • この記事ではこれについて扱う
    • コールグラフを通じてリクエストに関するデータを渡すデータの置き場所

Contextパッケージのソースコード

  • 主にcontext.goの内容を深掘りしていく

インターフェース

type Context interface {
       Deadline() (deadline time.Time, ok bool)
       Done() <-chan struct{}
       Err() error
       Value(key interface{}) interface{}
}
  • Done()
    • 完了を知らせるチャンネル
    • 完了している場合はこのチャンネルから値を呼び出せるようになる
    • 以下のようにselectの中で使うことが想定されている

func Stream(ctx context.Context, out chan<- Value) error {

   for {
      v, err := DoSomething(ctx) |
      if err != nil { |
         return err 
      } 
   select {
      case <-ctx.Done(): 
         return ctx.Err() 
      case out <- v: 
      } 
   }
} 
  • Err()
    • 完了している場合、Err()に完了理由が設定される
    • 完了理由はcontext.go#L156-L157で以下のように定義されている

var Canceled = errors.New("context canceled")

外部から呼び出される関数

  • 全部で6つの関数があり、3種類に分けられる

Contextを生成する関数

  • Background()
    • context.go#L204-L210
    • これを呼び出すことでContextが生成される
      • 実態はemptyCtxと名付けられたint型
      • インターフェースを満たしている
      • 名前通り空のContextである
    • 全てはここから始める
  • TODO()
    • context.go#L212-L218
    • Background()と同様に空のContextを生成するが、実装途中などで使用するContextが定まってない場合に暫定的に使うもの

キャンセル処理に関する関数

  • WithCancel()
    • context.go#L226-L239
    • Contextを受け取り、Contextとcancel()関数を返す
      • このcancel()を実行することでContextのDoneチャンネルが閉じられる(Done()チャンネルから値を呼び出せるようになる)
    • cancelCtxというContextを生成し、それを用いてpropagateCancel()関数を呼び出す
      • propagateCancel()は親のCotextがキャンセルされていた場合に文字通り子Cotextにもキャンセルを伝搬させる働きがある
  • WithDeadline()

    • context.go#L421-L456
    • 基本的にはWithCancel()と同じだが、受け取る引数に時刻が追加されている
    • 戻り値となっているcancel()は、指定された時刻になったときに実行されるようになっている
    • timerCtxというContextを生成し、それを用いてpropagateCancel()関数を呼び出す
  • WithTimeout()

    • context.go#L492-L504
    • 基本的にはWithDeadline()と同じだが、受け取る引数が時間になっている(タイムアウトさせるまでの時間)
    • コードを読むと自明だが、現在時刻に受け取った時間を足し、WithDeadline()を呼び出しているだけ
  • WithValue()

    • context.go#L506-L530
    • 深入りしないが、Contextで値を渡すときに使用するもの
    • 使用する例として、ヘッダーから抽出されたユーザーID、CookieまたはセッションIDに関連付けられた認証トークンなどが挙げられている

cancel()について

  • context.go#L392-L419
  • WithCancel()WithDeadline()WithTimeout()のいずれからも呼ばれるcancel()を深掘りしてみる
  • ソースコードは以下の通り
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}
  • 6行目 if c.err != nil
    • errには処理が完了した場合にその理由が詰められる
    • そのためnilでない場合はすでに完了している場合なので、そのままreturnしている
  • 10行目 c.err = err
    • ここで完了理由を設定するS
  • 12行目 c.done = closedchan
    • doneclosedchan(チャンネルのスライス)を設定する
    • これによりDone()チャンネルから値が呼び出せるようになる
  • 16行目〜 for child := range c.children
    • 子どものContext分だけfor文をまわし、その中でcancel()を再起的に呼び出している
    • これにより全ての処理を完了させることができる
  • 24行目 removeChild(c.Context, c)
    • 親のContextから子どものContextを切り離す

使い方

  • Go言語による並行処理の4章 Goでの並行処理パターン 4.12 contextパッケージで紹介されているソースコード(P139,140)を抜粋して用いる
    • 元のコードではmain()からprintGreeting()printFarewell()を呼び出しているが、printGreeting()に関する部分のみを抜粋した
  • このコードでは1秒でタイムアウトが発生し処理がキャンセルされるようになっている
  • 出力結果は以下の通り
    • cannot print greeting: context canceled
package main

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

func main() {
    var wg sync.WaitGroup
    // context.Background()でContextを生成し、WithCancelに渡す
    // 戻り値は新たに生成されたCotextとcancel()関数
    ctx, cancel := context.WithCancel(context.Background())
    // 必ずキャンセル処理が実行されるようdeferに指定しておく
    defer cancel()

    wg.Add(1)
    go func() {
        defer wg.Done()
        // 並行処理にContextを渡す
        if err := printGreeting(ctx); err != nil {
            fmt.Printf("cannot print greeting: %v\n", err)
            // printGreeting()からエラーが返ってきたらContextをキャンセルする
            cancel() 
        }
    }()

    wg.Wait()
}

func printGreeting(ctx context.Context) error {
    // 後続の処理にContextを渡す
    greeting, err := genGreeting(ctx)
    if err != nil {
        return err
    }
    fmt.Printf("%s world!\n", greeting)
    return nil
}

func genGreeting(ctx context.Context) (string, error) {
    // 親のContextをWithTimeout()に渡してContextを再生成する
    // このctxを受け取るlocal()は1秒でタイムアウトするので、1秒後にcancel()が実行される
    // ここで設定しているキャンセル処理は親のContextには影響を与えない
    ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
    defer cancel()

    switch locale, err := locale(ctx); {
    case err != nil:
        return "", err
    case locale == "EN/US":
        return "hello", nil
    }
    return "", fmt.Errorf("unsupported locale")
}

func locale(ctx context.Context) (string, error) {
    select {
    // タイムアウトが発生するとctx.Done()から値を呼び出すことができ、ここでctx.Err()で完了理由を返す
    case <-ctx.Done():
        return "", ctx.Err()
    case <-time.After(1 * time.Minute):
    }
    return "EN/US", nil
}

参考

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1