Go 1.16リリース記念連載の最終回はsignal.NotifyContext()
です。
ご存知のように、Go 1.7でcontext.Context
が入ってから、少しずついろいろなAPIがContext対応になりました。
- 1.7
net
のDialer
がDialContext()
メソッドを追加net/http
のhttp.Request
がContext()
とWithContext()
メソッドを追加。os/exec
がCommandContext
を追加
- 1.8
database/sql
が大幅にcontext.Context
対応を追加net/http
のhttp.Server
がShutdown()
を追加net
にcontext.Context
に対応したリゾルバーを追加
- 1.13
net/http
のNewRequestWithContext
と、Request.Clone()
が追加
外部へのネットワークアクセスや、外部プロセス起動など、戻ってくる時間が未知数なものはContextを受け入れる口があります。
もともとnet.Conn
にはタイムアウトがありましたが、Contextを受け入れるAPIは共通インタフェースでタイムアウトの設定をしたり、外部呼び出しをキャンセルできるようになります。また、複数のAPIアクセスを並行して行う場合にも同じContextを渡すことで一括でタイムアウト設定したり、キャンセルできるようになります。
Goは例外処理(大域脱出による処理の中断と、それにともなうリソース開放)はありませんが、並行処理に対応した一括処理中断は、並行処理時代の例外処理と言えると思います。現代においては、外部API呼び出しやデータベースアクセス、外部プロセス起動を行うようなロジックを作成する場合、Contextを第一引数として取るように設計するのが紳士淑女の嗜みです。
すべてのキャンセル処理をContextで統一すると一括処理中断がしやすくなるので、異常処理が中央集権的になって、見通しが良くなります。Doneというメソッド名で終了判断のselect等で利用できてコードがみやすくなるので、僕は実行フローに関係するような非同期の情報伝達は全部Context化していました。
一方でユーザー主導のキャンセルのトリガーとなるシグナルはContextのAPIがなく、チャネルの提供でした。そのため、次のような関数を作ってContextに合わせるというのを1.15まではする必要がありました。
func signalContext() (context.Context, func()) { |
1.16ではsignal.NotifyContext()
が入ったので、このような関数を手作りする必要がなくなりました。タイムアウトでもシグナルでも終了するような行儀の良いプログラムは次のように書けます。数値を変えてタイムアウトさせたり、正常終了させたり、シグナルを送ってみたり、いろいろ遊んでみてください。
package main |
err
の周りの処理がif文が増えています。cmd.Run()
のエラーは、タイムアウトでも必ずsignal: killedとなってしまいます。これはexec.CommandContextがタイムアウト時にプロセスにシグナルを送ってkillするためです。そのシグナルの理由が本当にユーザーが子プロセスに向けてKILLを送ったのか、Goのランタイムが送ったものなのかを区別するにはContext側のエラーを見る必要があります。
Goが良く使われるようなウェブサービスをシグナルでgracefulにシャットダウンするのはこんな感じになるでしょうか
package main |
これまでシグナル処理をサボっていた人も、Contextはちょっと面倒と思っていた人も、ぜひsignal.NotifyContext()
でぜひハッピーシグナルライフを送ってください。Goユーザーは行儀が良い、という評判をみんなで広めましょう。