Go で 無限ループをタイムアウトもしくは、Ctr+C で終了させたい

Go で、非同期リクエストを実施した後に、Pollingの処理を書いている。ふと気づいたのだが、その処理は無限ループの処理で、タイムアウトを設定している。しかし、タイムアウトまで長いので処理を中断させたいケースはままあるのだが、そういう時はどうするのだろう？

# シグナルを受信する

最初に必要なのは、`Ctr+c`のシグナルを受信することだ。`signal.Notify` の関数が使える。

* [Package signal](https://golang.org/pkg/os/signal/)

```golang
func Notify(c chan<- os.Signal, sig ...os.Signal)
```

`os.Signal` のチャネルを作って、受信したいシグナルを列挙すればよさげ。ちなみに、シグナルを指定しないと、すべてのシグナルを受信するのがデフォルトになります。

## シグナルの種類

受信できるシグナルの種類ですが、Go の場合こちらに書いてあります。

* [package syscall](https://golang.org/pkg/syscall/)

Linuxのシグナルと同じになっていますが、そもそもLinux のシグナルにどんなものがあったのか覚えていません。

* [signal](https://linuxjm.osdn.jp/html/LDP_man-pages/man7/signal.7.html)

上記のマニュアルを見ると、SIGINT が該当しそうです。コマンドラインツールなので、SIGHUPも受信したほうがいいかなぁ、、、と思ったけど（例えば無限ループしている状態で、Terminal落としたとか）、その場合は、コマンドのプロセスごと死ぬからまあいいかと。

> シグナル	値	動作	コメント
SIGHUP	 1	Term	制御端末(controlling terminal)のハングアップ検出、
または制御しているプロセスの死
SIGINT	 2	Term	キーボードからの割り込み (Interrupt)
SIGQUIT	 3	Core	キーボードによる中止 (Quit)
SIGILL	 4	Core	不正な命令
SIGABRT	 6	Core	abort(3) からの中断 (Abort) シグナル
SIGFPE	 8	Core	浮動小数点例外
SIGKILL	 9	Term	Kill シグナル
SIGSEGV	11	Core	不正なメモリー参照
SIGPIPE	13	Term	パイプ破壊:
読み手の無いパイプへの書き出し
SIGALRM	14	Term	alarm(2) からのタイマーシグナル
SIGTERM	15	Term	終了 (termination) シグナル
SIGUSR1	30,10,16	Term	ユーザー定義シグナル 1
SIGUSR2	31,12,17	Term	ユーザー定義シグナル 2
SIGCHLD	20,17,18	Ign	子プロセスの一時停止 (stop) または終了
SIGCONT	19,18,25	Cont	一時停止 (stop) からの再開
SIGSTOP	17,19,23	Stop	プロセスの一時停止 (stop)
SIGTSTP	18,20,24	Stop	端末より入力された一時停止 (stop)
SIGTTIN	21,21,26	Stop	バックグランドプロセスの端末入力
SIGTTOU	22,22,27	Stop	バックグランドプロセスの端末出力

## SIGINT 受信で終了する際の終了ステータスコード

SIGINTを受け付けて終了する場合のステータスコードって何になるのでしょう？下記のブログにとてもいい表がありました。

* [終了コードの識別とそれぞれの意味 - Circle CI support ](https://support.circleci.com/hc/ja/articles/360002341673-%E7%B5%82%E4%BA%86%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E8%AD%98%E5%88%A5%E3%81%A8%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E3%81%AE%E6%84%8F%E5%91%B3)

上記ブログより

| 終了コード | 意味 | 例 | コメント |
| --------- + ------- + ------- + ------ |
| 1 | 一般的なエラーの catch-all	| let "var1 = 1/0" | "divide by zero" や、他の許容されない操作を含む各種のエラー |
| 2 | シェル内蔵機能の誤使用 (Bash ドキュメントに従う) | empty_function() {}| キーワードが見つからないまたはコマンドが存在しない、またはアクセス許可の問題 (および、バイナリファイル比較の失敗時に diff が返すリターンコード) |
| 126 |	起動されたコマンドが実行できない |	/dev/null | アクセス許可の問題、またはコマンドが実行形式ファイルでない |
| 127 | "command not found" | illegal_command | $PATH の問題、またはタイプミス |
| 128 | exit への無効な引数 | exit 3.14159 | exit は 0 ～ 255 の範囲の整数値のみを引数として受け付けます (最初の脚注を参照)| 
| 128+n | 致命的なエラー信号 "n" | kill -9スクリプトの $PPID | $?137 (128 + 9) を返す |
| 130 |	スクリプトが Control-C により終了された | Ctl-C | Control-C は致命的なエラー信号 2 (130 = 128 + 2、上記参照)|
| 255* | 終了ステータスの範囲外 | exit -1	| exit は 0 ～ 255 の範囲の整数値のみを引数として受け付けます|

これでいくと今回は `130` でExit したらよさそうですが、まずい、今まで問題があったら `1`　で終了させていた。（CIで使うことを想定したツールだったので、終了させたかった）でも本当は終了ステータスはいろいろだから、ちょっとこの辺はライブラリ書いたほうがよさそう（もしくは、ライブラリを探すか、、、）

## サンプルコード

サンプルコードは次の感じです。

```golang
	quit := make(chan os.Signal)
	signal.Notify(quit, os.Interrupt)
	fmt.Println("step1")
	<-quit
	fmt.Println("step2")
```

こちらを実行すると、step1 のみが最初に表示されて、`ctr+c`を受信したら step2が表示されて終了します。Notifyは、処理をブロックしませんのので、こんな感じで書けそうです。

# タイムアウトと、複数のチャネルを受信する

シグナルの受信の方法はわかったのですが次の課題は、複数のチャネルを扱う方法です。シグナルを受信したら、チャネル経由で受け取るわけですが、Pollingの無限ループは現在も、タイムアウトの処理が必要で、尚且つ、現在は無限ループを go routine でラップしていて、チャネルを受け取っているので、複数のチャネルの処理が必要です。

結論からいくと、go lang の `select` は相当強力で、複数のチャネルの受信も書くことができます。

```golang
func main() {
	timeout := 30
	quit := make(chan os.Signal)
	c := make(chan string, 1)
	signal.Notify(quit, os.Interrupt)
	go func() {
		result := loop()
		c <- result
	}()
	select {
	case result := <-c:
		fmt.Println(result)
	case <-quit:
		fmt.Println("Interrupt signal accepted.")
	case <-time.After(time.Duration(30) * time.Second):
		fmt.Printf("Timeout happened. exceed %d second.\n", timeout)
	}
}

func loop() string {
	counter := 0
	for {
		time.Sleep(5 * time.Second)
		counter++
		fmt.Println("Do my job:" + strconv.Itoa(counter))
		if counter == 50 {
			return "done"
		}
	}
}
```

### 実行例

```bash
$ go run main.go
Do my job:1
Do my job:2
Do my job:3
Do my job:4
Do my job:5
Timeout happened. exceed 30 second.
```

`ctr+c` 

```bash
$ go run main.go
Do my job:1
Do my job:2
Do my job:3
Interrupt signal accepted.
```


## タイムアウト処理

タイムアウト処理を解説すると`time.After`関数を使っています。一定期間が過ぎると、チャネルに送信します。ちなみに、このマニュアルを読んでいると、効率のためには、`NewTimer` を使えとあります。

```golang
func After(d Duration) <-chan Time
```

折角なのでこちらも試してみましょう。戻りがTimerなので、どうやってチャネルを待つのだろうと思ったのですが、Timerのストラクトに、Cというチャネルを持っているのでそれを使えば良さげです。

```golang
func NewTimer(d Duration) *Timer
```

先ほどのプログラムを一部変更

```golang
	// case <-time.After(time.Duration(30) * time.Second):
	case <-time.NewTimer(time.Duration(30) * time.Second).C:
		fmt.Printf("Timeout happened. exceed %d second.\n", timeout)
```

### 実行結果

同じように動作しているようです。

```bash
$ go run main.go
Do my job:1
Do my job:2
Do my job:3
Do my job:4
Do my job:5
Timeout happened. exceed 30 second.
```

# リソース

ちなみに今回の調査で、go routine のパターンのブログを見つけたので貼っておきます。

* [Go の channel 処理パターン集](https://hori-ryota.com/blog/golang-channel-pattern/)

go routine と select はもうちょい勉強せなあかんなぁ。