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

# Contextとは
- タイムアウト処理やgoroutineの処理停止を統一的に扱うためのパッケージ
- 1.7から標準パッケージとして導入された
    - [公式ドキュメント](https://golang.org/pkg/context/)
    - [ソースコード](https://github.com/golang/go/tree/master/src/context)
- 提供されている機能としては以下の2つ
    - コールグラフの各枝をキャンセルするAPI
        - この記事ではこれについて扱う
    - コールグラフを通じてリクエストに関するデータを渡すデータの置き場所

# Contextパッケージのソースコード
- 主に[context.go](https://github.com/golang/go/blob/master/src/context/context.go)の内容を深掘りしていく

## インターフェース
- [context.go#L58-L154](https://github.com/golang/go/blob/master/src/context/context.go#L58-L154)
- 定義は以下のようになっている

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

```go

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](https://github.com/golang/go/blob/b1b67841d1e229b483b0c9dd50ddcd1795b0f90f/src/context/context.go#L156-L157)で以下のように定義されている

```go

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

## 外部から呼び出される関数
- 全部で6つの関数があり、3種類に分けられる

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

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

- `WithCancel()`
    - [context.go#L226-L239](https://github.com/golang/go/blob/master/src/context/context.go#L226-L239)
    - Contextを受け取り、Contextと`cancel()`関数を返す
        - この`cancel()`を実行することでContextのDoneチャンネルが閉じられる(`Done()`チャンネルから値を呼び出せるようになる)
    - `cancelCtx`というContextを生成し、それを用いて`propagateCancel()`関数を呼び出す
        - `propagateCancel()`は親のCotextがキャンセルされていた場合に文字通り子Cotextにもキャンセルを伝搬させる働きがある
- `WithDeadline()`
    - [context.go#L421-L456](https://github.com/golang/go/blob/master/src/context/context.go#L421-L456)
    - 基本的には`WithCancel()`と同じだが、受け取る引数に時刻が追加されている
    - 戻り値となっている`cancel()`は、指定された時刻になったときに実行されるようになっている
        - `time.AfterFunc`で`cancel()`を指定している
        - [context.go#L444-L447](https://github.com/golang/go/blob/master/src/context/context.go#L444-L447)
    - `timerCtx`というContextを生成し、それを用いて`propagateCancel()`関数を呼び出す

- `WithTimeout()`
    - [context.go#L492-L504](https://github.com/golang/go/blob/master/src/context/context.go#L492-L504)
    - 基本的には`WithDeadline()`と同じだが、受け取る引数が時間になっている(タイムアウトさせるまでの時間)
    - コードを読むと自明だが、現在時刻に受け取った時間を足し、`WithDeadline()`を呼び出しているだけ
- `WithValue()`
    - [context.go#L506-L530](https://github.com/golang/go/blob/master/src/context/context.go#L506-L530)
    - 深入りしないが、Contextで値を渡すときに使用するもの
    - 使用する例として、ヘッダーから抽出されたユーザーID、CookieまたはセッションIDに関連付けられた認証トークンなどが挙げられている


## `cancel()`について
- [context.go#L392-L419](https://github.com/golang/go/blob/master/src/context/context.go#L392-L419)
- `WithCancel()`、`WithDeadline()`、`WithTimeout()`のいずれからも呼ばれる`cancel()`を深掘りしてみる
- ソースコードは以下の通り

```go
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`
    - `done`に`closedchan`(チャンネルのスライス)を設定する
    - これにより`Done()`チャンネルから値が呼び出せるようになる
- 16行目〜 `for child := range c.children`
    - 子どものContext分だけfor文をまわし、その中で**`cancel()`を再起的に呼び出している**
    - これにより全ての処理を完了させることができる
- 24行目 `removeChild(c.Context, c)`
    - 親のContextから子どものContextを切り離す

# 使い方
- [Go言語による並行処理](https://www.oreilly.co.jp/books/9784873118468/)の4章 Goでの並行処理パターン 4.12 contextパッケージで紹介されている[ソースコード](https://github.com/kat-co/concurrency-in-go-src/blob/master/concurrency-patterns-in-go/the-context-package/fig-greeter-with-context.go)(P139,140)を抜粋して用いる
    - 元のコードでは`main()`から`printGreeting()`と`printFarewell()`を呼び出しているが、`printGreeting()`に関する部分のみを抜粋した
- このコードでは1秒でタイムアウトが発生し処理がキャンセルされるようになっている
- 出力結果は以下の通り
    - `cannot print greeting: context canceled`

```go
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
}
```

# 参考
- [『Go言語による並行処理』 オライリー・ジャパン Katherine Cox-Buday 著、山口能迪 訳 ISBN 978-4-87311-846-8](https://www.oreilly.co.jp/books/9784873118468/)
- [『改訂2版 みんなのGo言語』 技術評論社 松木雅幸,mattn,藤原俊一郎,中島大一,上田拓也,牧大輔,鈴木健太 著 ISBN 978-4-297-10727-7](https://gihyo.jp/book/2019/978-4-297-10727-7)