Go で init() 内の os.Exit(1) を go test で回避する方法
概要
AWS Lambda Go プロジェクトを SAM で構築していた際、
パラメータストアから取得する処理を init()
に記述しました。
理由はパラメータストアから取得する処理をキャッシュし、再利用することで連続 Lambda の実行コストを節約する為です。
詳細は AWS Lambda ベストプラクティス参照
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/best-practices.html
実行環境の再利用を活用して関数のパフォーマンスを向上させます。 関数ハンドラー外で SDK クライアントとデータベース接続を初期化し、静的なアセットを /tmp ディレクトリにローカルにキャッシュします。関数の同じインスタンスで処理された後続の呼び出しは、これらのリソースを再利用できます。これにより、実行時間とコストが節約されます。
以下の様に init()
でパラメータストアから秘匿情報を取得する処理をキャッシュし、コスト節約したいと考えました。
1 | func init() { |
上記コードについて
GitHub Actions で go test
を実行しテストをしていますが、log.Fatal
で os.Exit(1)
発生し処理が停止します。
credentials が設定されてないというエラーです。
1 | NoCredentialProviders: no valid providers in chain. Deprecated. |
GitHub Actions でダミーの credentials を設定しても失敗します。
1 | run: go test -v -count=1 -race -cover -coverprofile=coverage ./... |
init()
でのキャッシュを諦めて、 handler
で処理するとハンドリングは簡単です。
ですが、「コスト節約」が頭から離れません。自分は弱い人間です。
go test
実行する時だけでも、この os.Exit(1)
を回避できないものか?ということで検証してみました。
検証
以下の様な main.go ファイルがあるとします。go test
実行時に init()
の log.Fatal
をどう回避するか、検証します。
1 | var n = 0 |
特に何も意識せず go test
すると init()
内の log.Fatal
で os.Exit(1)
が発生し強制終了される。
テストが完結しない。
テスト実行時にだけ var n = 1
とできたら良さそうだが、、、
main_test.go で main.go の処理を上書きできるか検証する
そもそも処理順序はどうなっているのか、まず検証してみた。
参考: https://github.com/kenzo0107/go-sample-order
1 | package pkg |
1 |
|
1 | func init() { |
go test
を実行してみます。
処理順序は以下の通りでした。
- pkg.var
- pkg.init
- main.var
- main.init
- test.init
- test.setup
以上から main_test.go
での如何なる処理も main.init
より先に実行できません。
main_test.go
で main.go
の変数 var n
を上書きする処理は難しそうです。
テスト実行時のみ環境変数で制御する
1 | func init() { |
log.Fatal
を logFatal
という関数に置換し、以下処理を実行する様にします。
- 環境変数 TEST が存在する →
log.Println
でログを残し、os.Exit(1)
を実行しない - 環境変数 TEST が存在しない →
os.Exit(1)
実施し強制停止
main.go を実行します。
1 | $ go run main.go |
環境変数 TEST=1 を設定し go test
を実行してみる。
1 | $ TEST=1 go test -v . |
無事 init()
内の os.Exit(1)
を回避し処理が継続して実行されました。
GitHub Actions の設定も簡易的な設定です。
1 | - name: Test |
環境変数でなく os.Args
をゴニョゴニョして go test
の実行を判断もできそうですが、実装的にはシンプルで用途としても他で使えるので LGTM かなと。
まとめ
本来 init()
でエラーハンドリングをすべきではないのかもしれません。
以下にも言及されていましたが、 init()
の処理で失敗したら以降の main()
の処理を実行しない、
というのは悪い処理でないように思います。
とはいえ、今回の環境変数で処理を操作、というのはややエレガントさに欠ける気持ちはあります。
やんごとなき事情がある場合にこの様な処理がある、ということを心のどこかに留めていただきたく、
ここで筆を置きたいと思います。
ご清聴ありがとうございました。
追伸
AWS 公式のドキュメントだと init()
で err を握り潰している!
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/golang-handler.html
エラーが発生する場合のテストどうするつもりだろう?
絶対エラー起きないんで!って言われるかな〜
Go で init() 内の os.Exit(1) を go test で回避する方法
https://kenzo0107.github.io/2021/01/31/2021-02-01-avoid-go-init-osexit/