初心者がGo言語のcontextを爆速で理解する ~ cancel編　~

# はじめに
久川善法です。
しばらく放置していた[context](https://godoc.org/golang.org/x/net/context)について勉強してみました。

この記事では[context](https://godoc.org/golang.org/x/net/context)の用途から、具体的な使い方までを自分用にまとめました。
※ 勉強中、参考になる記事が多くあったので、記事の最後にURLを載せています。

# コンテキストとは？
[context](https://godoc.org/golang.org/x/net/context)はGo1.7で標準パッケージに仲間入りしたライブラリです。

[Go言語による並行処理](https://www.amazon.co.jp/dp/4873118468)によると、
[context](https://godoc.org/golang.org/x/net/context)が仲間入りする以前のGo言語での並行処理では、
doneチャネルを使って並行処理を終了させていました。

しかし、doneチャネルのみを使った方法だとキャンセル理由の伝搬やタイムアウトなど複雑になる箇所がありました。
そこで[context](https://godoc.org/golang.org/x/net/context)が実験的に生まれて、最終的に標準パッケージとして活用されるようになったそうです。

# コンテキストの主な2つの用途
[context](https://godoc.org/golang.org/x/net/context)の主な用途は2つあります。

- **Goroutineの適切なキャンセル**
- リクエスト情報の伝搬

この記事では1つ目の**Goroutineの適切なキャンセル**にフォーカスします。
リクエスト情報の伝搬については、また次の記事でまとめます。

## Goroutineの適切なキャンセル
[context](https://godoc.org/golang.org/x/net/context)のキャンセルの役割として、
**タイムアウトやデッドラインを設定し後続の処理が停滞するのを防ぎ、不要になったGoroutineはキャンセルしてリソースの解放をする役割**があります。

タイムアウトやデッドラインを設定しないと、接続などの問題で遅い処理があった際に、後続の処理を停滞させてしまいます。
そこで、[context](https://godoc.org/golang.org/x/net/context)で事前にタイムアウト時間3秒などの設定すると、リトライ処理や他の手段を選択できるようになります。

また、Goroutineはキャンセルせずに放っておくと、消えずに残ってリソースを消費することがあります。
これも[context](https://godoc.org/golang.org/x/net/context)でちゃんとキャンセルしてリソースを解放して、他の処理にそのリソースを使えるようにできます。

また、**Goroutineをキャンセルしたいパターンは3つ**あると思います。

1. 全てのGoroutineをキャンセルしたい場合
- 分岐した先のGoroutineをキャンセルしたい場合
- ブロック(遅い処理)している処理をキャンセルしたい場合

ここから具体的にコードをみながら、これらの場合の対応をまとめます。

# コンテキストの使い方ざっくり
まずはドキュメントから[context](https://godoc.org/golang.org/x/net/context)の使い方を学んでいきます。
[context](https://godoc.org/golang.org/x/net/context)を読んだ方が早いですが一応説明をします。

## Contextの型

```golang
type Context interface {
    // Deadlineが設定されている場合はその時刻を返却。ok==falseの時は未設定
    Deadline() (deadline time.Time, ok bool)

    // このコンテキストがキャンセルされたりタイムアウトした場合にcloseされます。
    Done() <-chan struct{}

    // Doneチャンネルが閉じた後なぜこのコンテキストがキャンセルされたかを知らせます。
    Err() error

    // Valueはkeyに紐付いた値を返し、設定した値がない場合はnilを返します。
    Value(key interface{}) interface{}
}
```

## Functionたち

- ```func WithCancel(parent Context) (ctx Context, cancel CancelFunc)```

WithCancelはparentのコピーを返します。
parent.Doneが閉じられるか、キャンセルが呼ばれるとコピーされたDoneチャンネルも閉じます。
イメージで例えると木構造になっていて、 生成元のContextがキャンセルされたときに、そこから派生した子のContextもすべてキャンセルされます。

- ```func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)```

WithDeadlineはparentのコピーを返します。
parent.Doneが閉じられるか、キャンセルが呼ばれるとコピーされたDoneチャンネルも閉じます。
Contextのdeadlineは現在時刻+timeoutか親の期限（木構造になってるから親が優先）のどちらか早いほうに設定されます。
アラームの時刻を設定してまつようなイメージ

- ```func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)```

WithTimeoutはparentのコピーを返します。
parent.Doneが閉じられるか、キャンセルが呼ばれるとコピーされたDoneチャンネルも閉じます。
Contextのtimeoutは指定した時間か親の期限（木構造になってるから親が優先）のどちらか早いほうに設定されます。
ストップウォッチで3秒まつようなイメージ

- ```func Background() Context```や```func TODO() Context```

どちらも空のContextを生成します。
基本的にBackground()を使うイメージですが、TODOを使う時がわかっていません。
（nilにはしたくないけど、あとで何か設定するといった時につかうのでしょうか？）

- ```type CancelFunc = context.CancelFunc```

Contextをキャンセルします。

# 具体的なコードベース
それでは、パターン別の対応を実際のコードと一緒にみていきます。

1. 全てのGoroutineをキャンセルしたい場合
- 分岐した先のGoroutineをキャンセルしたい場合
- ブロック(遅い処理)している処理をキャンセルしたい場合

## 1. 全てのGoroutineをキャンセルしたい場合

```golang
func main() {
	// contextを生成
	ctx := context.Background()

	// 親のcontextを生成し、parentに渡す
	ctxParent, cancel := context.WithCancel(ctx)
	go parent(ctxParent, "Hello-parent")

	// parentのcontextをキャンセル。mainを先に終了させないように1秒待ってから終了
	cancel()
	time.Sleep(1 * time.Second)
	fmt.Println("main end")
}

func parent(ctx context.Context, str string) {
	// parentからcontextを生成し、childに渡す
	childCtx, cancel := context.WithCancel(ctx)
	go child(childCtx, "Hello-child")
	defer cancel()
	// 無限ループ
	for {
		select {
		case <-ctx.Done():
			fmt.Println(ctx.Err(), str)
			return
		}
	}
}

func child(ctx context.Context, str string) {
	// 無限ループ
	for {
		select {
		case <-ctx.Done():
			fmt.Println(ctx.Err(), str)
			return
		}
	}
}
// context canceled Hello-child
// context canceled Hello-parent
// main end
```

ちゃんと木構造のように親のcontextがcancelされて、次に子のcontextが・・・と伝搬されているのがわかります。

## 2. 分岐した先のGoroutineをキャンセルしたい場合
```golang
func main() {
	// contextを生成
	ctx := context.Background()

	// 親のcontextを生成し、parentに渡す
	ctxParent, cancel := context.WithCancel(ctx)
	go func() {
		err := child(ctxParent)
		if err != nil {
			fmt.Println("*parent err*")
			cancel()
			return
		}
	}()

	// 無限ループ
	for {
		select {
		case <-ctxParent.Done():
			fmt.Println(ctxParent.Err(), "*parent done*")
			return
		default:
			fmt.Println("parent process")
		}
		time.Sleep(1 * time.Second)
	}
}

func child(ctx context.Context) error {
	// parentからcontextを生成し、childに渡す
	childCtx, cancel := context.WithCancel(ctx)
	go func() {
		if err := getErr(); err != nil {
			fmt.Println("*child err*")
			// childCtxをcancel
			cancel()
		}
	}()

	// 無限ループ
	for {
		select {
		case <-childCtx.Done():
			fmt.Println(childCtx.Err(), "*child done*")
			time.Sleep(4 * time.Second)
			return errors.New("")
		default:
			fmt.Println("child process")
		}
		time.Sleep(1 * time.Second)
	}
}

func getErr() error {
	time.Sleep(2 * time.Second)
	return errors.New("")
}

// parent process
// child process
// child process
// parent process
// *child err*
// context canceled *child done*
// parent process
// parent process
// parent process
// parent process
// *parent err*
// context canceled *parent done*

```
コードの流れを説明します。
parent process と child processを同時に動かします。
2秒後にcancel()を呼び出しchild processは終了させ、そのあとにparent processをcancel()しています。
このように、分岐先で問題があった場合は、分岐先のgoroutineのみ終了させることができます。

## 3. ブロック(遅い処理)している処理をキャンセルしたい場合
```golang
func main() {
	ctx := context.Background()

	// 3秒後をデッドラインにする
	ctx, cancel := context.WithDeadline(ctx, time.Now().Add(3 * time.Second))
	defer cancel()

	go sayDeadLine(ctx)

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

func sayDeadLine(ctx context.Context) {
	for {
		fmt.Println(ctx.Deadline())
		time.Sleep(1 * time.Second)
	}
}

// 2018-12-08 12:02:06.892819525 +0900 JST m=+3.000593144 true
// 2018-12-08 12:02:06.892819525 +0900 JST m=+3.000593144 true
// 2018-12-08 12:02:06.892819525 +0900 JST m=+3.000593144 true
// context deadline exceeded
```

コードの流れを説明します。
3秒後にcancelが呼ばれるようにdeadlineを設定します。
1秒に一回無限ループを回し、deadlineの設定を表示します。
3秒後にcancelが呼ばれ処理が終了します。

# 注意すること
- contextを構造体の中に定義することはだめ
例えば、contextを含んだpublicな構造体を定義すると、いろんなところからcontexを使えます。
しかし、意図しないcancelや親子関係が複雑になってしまいます。
なので第一引数で渡していく方法が推奨されています。

- contextを第一引数に
引数に渡す際は必ず第一引数に渡すのが決まりのようです。


# 最後に
contextの用途や具体的な使い方を学びました。

今回の記事では「Goroutineの適切なキャンセル」について学んだので
次回は「リクエスト情報の伝搬」について学んでいきます。

# 参考資料
・[Goの並行パターン：コンテキスト (Go Concurrency Pattern: Context)](https://www.ymotongpoo.com/works/goblog-ja/post/context/)
・[Go1.7のcontextパッケージ](https://deeeet.com/writing/2016/07/22/context/)
・[golangでcontextパッケージを使う](https://twinbird-htn.hatenablog.com/entry/2017/04/07/214420)
・[Go言語による並行処理](https://www.amazon.co.jp/dp/4873118468)