はじめに
自動化やツール開発において、通常時に上手くいくのは当たり前です。大切なのは失敗を想定することです。自動化したツールがエラーも出さずに実行結果的にも成功してるので動いていると思っていたら、実は問題が発生していて泣いた経験は、多くの人にあるのではないでしょうか。エラーを出力し、適切に失敗させて、ログに記録することで、問題の早期発見と迅速な対応が可能になります。また、エラーが発生する可能性のある箇所を事前に想定し、適切に処理することで、ツールの信頼性と安定性が向上します。
しかし、エラーハンドリングができていても、それだけでは不十分です。優れた自動化ツールは、環境の変化に柔軟に対応できるようにコードが設計されているべきです。
また、自動化ツールの完成度を高めるには、エラーハンドリングだけでなく、保守性、拡張性、ユーザビリティなども考慮する必要があります。
自動化ツールを開発する際は、常に不安を抱きながらコードを書くことが重要です。「もしこの部分が失敗したらどうなるだろう」「これで本当に大丈夫だろうか」と自問自答しながら、エッジケースを想定し、想定外のエラーが発生した場合の対策を講じておくことが求められます。
本記事では、Golang とシェルスクリプトを例に、エラーハンドリングの具体的な方法や、自動化ツール開発における留意点のいくつかについて解説していきます。はじめにで触れた内容の一部については、詳細な説明を割愛していますので、ご了承ください。
Golang でのエラーハンドリング
Golang には例外機構はありませんが、関数の戻り値として error を返すのが一般的です。以下は、引数のバリデーションをして、想定外ならエラーを返す例です。
type MyOption struct { IsHoge bool Fuga int } func f(s string, o MyOption) error { if !regexp.MustCompile(`^A-\d{4,8}$`).MatchString(s) { return fmt.Errorf("invalid argument: s must be in format A-\\d{4,8}") } if o.IsHoge && o.Fuga == 0 { return fmt.Errorf("invalid argument: o.Fuga is required if o.IsHoge is true") } // 処理本体... return nil }
このように、関数の先頭で引数をバリデーションし、想定外ならエラーを返すようにしています。また、エラーメッセージは fmt.Errorf
を使って生成しています。これは、エラーメッセージをフォーマットする際のベストプラクティスです。
さらに、エラーが発生した場合は、適切にエラーを処理することが大切です。以下は、エラーをログ出力し、さらに上位の関数に返している例です。
func doSomething() error { err := f("A-1234", MyOption{IsHoge: true, Fuga: 1}) if err != nil { log.Printf("failed to call f: %v", err) return fmt.Errorf("failed to do something: %w", err) } // 処理続行... return nil }
ここでは、f
関数でエラーが発生した場合、ログ出力をしつつ、fmt.Errorf
を使ってエラーをラップして返しています。%w
は Go 1.13 から導入された Wrapping Verb で、元のエラーを内包した新しいエラーを生成します。これにより、エラーの因果関係が明確になり、デバッグがしやすくなります。
シェルスクリプトでのエラーハンドリング
シェルスクリプトでも、コマンドの実行結果を適切にチェックし、エラーを検出することが重要です。以下は、コマンドの実行結果をチェックする一般的なパターンです。
some_command if [ $? -ne 0 ]; then echo "some_command failed" exit 1 fi
$?
は直前のコマンドの終了ステータスを表す特殊変数です。0 であれば成功、0 以外であればエラーを表します。
また、あるコマンドの実行結果を別のコマンドにパイプで渡す場合、パイプラインの途中でエラーが発生してもシェルスクリプトは中断されません。これを防ぐには、以下のようにします。
set -o pipefail some_command | another_command if [ $? -ne 0 ]; then echo "pipeline failed" exit 1 fi
set -o pipefail
は、パイプラインのいずれかのコマンドが0以外の終了ステータスを返した場合、パイプライン全体の終了ステータスをそのコマンドの終了ステータスにするオプションです。
外部リソースのエラーハンドリング
自動化ツールでは、外部APIやデータベースへのアクセスが頻繁に行われます。これらの外部リソースは、ネットワークの問題などで必ず失敗する可能性があることを念頭に置く必要があります。
Golang では、net/http
パッケージを使った HTTP リクエストが一般的です。以下は、タイムアウトを設定し、レスポンスのステータスコードをチェックする例です。
client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get("https://3-shake.com/") if err != nil { return fmt.Errorf("failed to get: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status code: %d", resp.StatusCode) } // レスポンスの処理...
シェルスクリプトでは、curl
コマンドがよく使われます。以下は、curl
コマンドのエラーをチェックする例です。
response=$(curl -s -w "%{http_code}" https://example.com/api/hoge) status_code=$(tail -n1 <<< "$response") # 最後の行がステータスコード body=$(sed '$d' <<< "$response") # 最後の行以外がレスポンスボディ if [ $status_code -ne 200 ]; then echo "unexpected status code: $status_code" exit 1 fi # レスポンスの処理...
-s
オプションでサイレントモードにし、-w "%{http_code}"
でレスポンスのステータスコードを出力しています。
デバッグをできるようにする
デバッグから逃げてはいけません。問題が発生した際に、どこで何が起きているのかを把握できることは重要です。
シェルスクリプトでは、bash -x
オプションを使うことでデバッグ出力を有効にできます(他のシェルでも似たようなオプションがある)。このオプションを付けてスクリプトを実行すると、各コマンドが実行される前に、そのコマンドが表示されます。これにより、スクリプトのどの部分が実行されているのか、変数がどのように展開されているのかを確認できます。
bash -x ./your_script.sh
Golang にも同様のデバッグ機能があります。delve
というデバッガを使うことで、Golang プログラムのデバッグが可能です。delve
を使えば、ブレークポイントを設定してプログラムを停止させ、変数の値を確認したり、ステップ実行したりできます。
# delve のインストール go get -u github.com/go-delve/delve/cmd/dlv # デバッグ実行 dlv debug ./your_program.go
デバッグ実行中は、break
コマンドでブレークポイントを設定し、continue
でプログラムを再開、next
で次の行に進むなどの操作ができます。
これらのデバッグ機能を活用することで、問題の原因をより迅速に特定できるようになります。
まとめ
- Golang では、関数の戻り値として error を返し、適切にエラーをハンドリングしよう。
- シェルスクリプトでは、コマンドの終了ステータスをチェックし、パイプラインのエラーにも対処しよう。
- 外部リソースは必ず失敗すると想定してコードを書こう。 タイムアウトの設定やエラーチェックを忘れずに。
- 自動化は常に不安であり、モニタリングすることを忘れずに。
- デバッグから逃げるな。
個人的に自動化に関してはエッジケースの処理を上手くやっていたりすると「オー」って思うのに問題がないので目立たないので今回は記事を書きました。しっかりと失敗することを想定することで、自動化ツールの信頼性と保守性は大きく向上します。 Golang とシェルスクリプトの両方で、エラーとうまく付き合っていきましょう。