Goのsignal.NotifyContextについて
背景
context.background()
上記のような形で空Contextを作成してwebサーバーを起動するコードを書いたらcopilotにレビューでsignal.NotifyContextを使う方が良いよみたいに言われた
恥ずかしながらsignal.NotifyContextをよく理解していなかったので説明して簡単にまとめてみる
説明
NotifyContext returns a copy of the parent context that is marked done (its Done channel is closed) when one of the listed signals arrives, when the returned stop function is called, or when the parent context's Done channel is closed, whichever happens first.
NotifyContextは、指定されたシグナルのいずれかが到着したとき、返されたstop関数が呼び出されたとき、または親コンテキストのDoneチャネルが閉じたときのいずれか最初に発生した時点で、完了(Doneチャネルが閉じられる)としてマークされる親コンテキストのコピーを返します。
つまり、指定していたOSのシグナルをキャッチしてキャンセレーションを行うことの出来るcontextを下記のように完結に作成する事ができるってことらしい
後述するNotifyContext以前のコードだとcontext作成とシグナルの監視の処理を別々で実装する必要があったのだが、それが一行で簡潔に定義できるのは嬉しい
func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)
キャンセレーションを行うメリット
そもそもキャンセレーション処理を行うメリットについてもまとめる
1. リソースの効率的な利用
不要になった処理を停止することで、CPU、メモリ、ネットワーク接続などのリソースが無駄に使われ続けるのを防ぐ
2. Graceful Shutdown(優雅なシャットダウン)
プログラムが外部からの終了要求を受けた際に、進行中の処理を優雅に中断し、後処理(クリーンアップ)を行う機会を与える
3. エラーハンドリングの簡素化
複数の処理が協調して動いている場合、ある処理が失敗や中断したら、それに依存する他のすべての処理も停止する必要がある。
キャンセルレーションが1つの失敗を複数の処理に効率的に伝播させるための共通の手段となるのだ
- 複数のデータベースに書き込みの処理を行う必要がある場合に一つ目の書き込み処理に失敗したときにキャンセレーションを使えば、失敗を検知した時点でcontextをキャンセルしてそれ以降の処理を自動的に停止させることが出来る。
4. タイムアウト処理の容易化
時間のかかる処理にタイムアウトを設定する際にもキャンセルレーションが役立つ
このケースではcontext.WithTimeoutを使うべきなので文脈は違うが、一応
実際の使用例
SIGINT (Ctrl+C) または SIGTERM(killコマンド)で処理を中断すると該当のメッセージが表示されることが確認できるサンプルコード
defer stop()を呼んでリソースを開放してあげるのを忘れずに
package main
import (
"context"
"fmt"
"os"
"os/signal"
"time"
)
func main() {
// シグナルを監視するコンテキストを作成
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer stop()
// 別の goroutine で時間のかかる処理をシミュレート
go func() {
select {
case <-time.After(10 * time.Second):
fmt.Println("処理が完了しました。")
stop()
case <-ctx.Done():
fmt.Println("シグナルを受信しました。処理を中断します。")
}
}()
<-ctx.Done()
}
signal.NotifyContext以前の書き方
以前まではsignal.NotifyでOSからのシグナルをキャッチしたら context.WithCancelでラップしておいたキャンセレーション可能なcontextにcancel()を実行してあげる処理を自分で用意する必要があった。
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// シグナル監視用のチャネル
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, os.Kill, syscall.SIGTERM)
// キャンセル可能なコンテキスト
ctx, cancel := context.WithCancel(context.Background())
// 別の goroutine でシグナルを待機し、受信したらコンテキストをキャンセル
go func() {
<-sigChan
fmt.Println("シグナルを受信しました。")
cancel()
}()
// 別の goroutine で時間のかかる処理をシミュレート
go func() {
select {
case <-time.After(10 * time.Second):
fmt.Println("処理が完了しました。")
cancel() // 処理が完了したら手動でキャンセル
case <-ctx.Done():
fmt.Println("シグナルを受信しました。処理を中断します。")
}
}()
<-ctx.Done() // コンテキストのキャンセルを待機
fmt.Println("メイン処理が終了しました。")
}
Discussion