Go言語のエラーハンドリング設計パターン集!現場で役立つ実践例
生徒
「Goでエラーをどう整理したらきれいに書けますか?」
先生
「Goではエラーを返す仕組みを使って、わかりやすく、再利用しやすく設計するのがポイントです。」
生徒
「具体的にはどんなパターンがありますか?」
先生
「エラー統一、ラップ(error wrapping)、共通処理への委譲などが定番になります。それぞれ見ていきましょう!」
1. エラー返却の統一パターン
Goでは、エラーは値として返す仕組みがあります。関数が失敗する可能性があるときは、戻り値の最後にerrorを置き、正常・異常で分けます。
func readConfig(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(data), nil
}
このように、エラー処理が一貫していて読みやすい構造になります。慣れないうちは「value, err := …」の形を覚えると良いでしょう。
2. error wrapping(エラーを包む)パターン
処理の流れが複数段階の場合、どの段階でエラーが発生したか分かりやすくするには、エラーをラップ(包む)と良いです。Go 1.13以降はfmt.Errorfで%wを使います。
func loadUser(id int) (*User, error) {
user, err := db.Find(id)
if err != nil {
return nil, fmt.Errorf("ユーザー取得失敗 id=%d: %w", id, err)
}
return user, nil
}
このようにすると、エラーを伝えながら原因を保持でき、ログやデバッグ時に便利です。
3. 共通エラー処理を関数にまとめる
エラーのログ出力や把握を共通化したいときは、関数でまとめて呼ぶとコードがスッキリします。
func handleError(err error, context string) {
log.Printf("エラー [%s]: %v", context, err)
}
func run() {
data, err := readConfig("config.json")
if err != nil {
handleError(err, "設定読み込み")
return
}
fmt.Println(data)
}
このパターンを使うと、どこでエラーがあっても同じ処理・ログ形式で管理できます。
4. カスタムエラー型による詳細情報の付加
より詳しい情報を伝えたいときは、自分専用のエラー型を作成し、必要情報を付加しましょう。
type MyError struct {
Code int
Msg string
}
func (e *MyError) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Msg)
}
func doTask() error {
return &MyError{Code: 42, Msg: "処理に失敗しました"}
}
起動時にif err, ok := err.(*MyError)のように型アサーションすれば、エラーの詳細な分岐処理も可能になります。
5. パターンを組み合わせた実践例
実際の現場では、上記パターンを組み合わせて使います。たとえば:
func process(id int) error {
user, err := loadUser(id)
if err != nil {
return fmt.Errorf("process内 loadUserエラー: %w", err)
}
// 処理続行…
return nil
}
func main() {
if err := process(7); err != nil {
handleError(err, "main process")
}
}
この例では、ラップ、共通処理利用、呼び出し元での表示、すべてがまとまり、現場でよく使われる形です。
6. 次に進むためのポイント
- 戻り値は「値, error」の形式で返す
- エラー発生時は早期リターンして処理を止める
- fmt.Errorf + %w でエラーをラップして流れを残す
- handleError のように共通処理にまとめてログや通知を整理する
- 必要に応じてカスタムエラー型で制御分岐を実装する
これらの基本パターンが揃っていれば、多くの実践環境で対応しやすくなります。
まとめ
Go言語のエラーハンドリングは、一見すると単純に「エラーを返すだけ」に見えますが、実際にはとても奥深い仕組みになっています。特に現場の開発では、コードの読みやすさや再利用性、そして障害時の原因追跡のしやすさが重要になるため、エラーの扱い方には一定の設計が求められます。本記事で紹介した「エラー返却の統一」「error wrapping」「共通処理化」「カスタムエラー型」「複合パターン活用」は、Go言語の開発現場で広く使われている考え方です。どれも特別なテクニックというより、Goが備えている仕組みを丁寧に組み合わせて活かす方法といえます。
特に、Goのエラーは「例外ではなく値として返す」という特徴があるため、開発者自身が「どう扱うべきか」を決める余地が大きく、その分、設計の工夫が品質に直結します。エラーの統一的な返し方を決めるだけでも読みやすさが大きく向上し、ラップを使えば処理の流れを追いやすくなり、共通処理を関数化すればメンテナンスが容易になります。また、複雑なエラーを扱う場合にはカスタムエラー型が非常に有効で、運用時のログ解析やユーザーへのメッセージ出し分けにも役立ちます。
さらに、Go言語ではエラー処理が明確にコードに表れるため、トラブルシューティングがしやすく、コードレビューでも意図を読み取りやすいメリットがあります。初心者のうちは「毎回 err を書くのが大変」と感じるかもしれませんが、慣れてくるとこの明示的なスタイルの良さがわかるはずです。実践的なプロジェクトでは、これらのエラーパターンを組み合わせて、より安全で安定した処理に仕上げていくことが求められます。
最後に、エラーハンドリングの理解を深めるための簡単なサンプルをもう一つ紹介します。これはラップと共通処理を組み合わせた小さな例ですが、日常的に使われる形です。
func readFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("ファイル読み込み失敗: %w", err)
}
return string(data), nil
}
func main() {
content, err := readFile("sample.txt")
if err != nil {
handleError(err, "main")
return
}
fmt.Println("読み込み成功:", content)
}
このように、Goのエラーハンドリングはさまざまな設計パターンを組み合わせることで、より実践的で安全なコードを実装できます。エラーを恐れるのではなく、「どう設計すれば扱いやすくなるか」を考えながら進めるのがポイントです。
生徒
「先生、今日の内容でGoのエラーハンドリングがだいぶ理解できました!特に、エラーをラップすると原因が追いやすくなるのが印象に残っています。」
先生
「そうですね。ラップは現場でもとてもよく使われる考え方ですし、ログから原因を特定するのにも役立ちます。あと共通処理化も大事なポイントです。」
生徒
「共通化するとコードがスッキリして読みやすくなりますね。カスタムエラーも便利そうですけど、どんな場面で使うんですか?」
先生
「例えば、エラーごとにエラーコードを使い分けたいときや、ユーザーに分かりやすく伝えたいときですね。運用時のログ分析にもとても有効なんですよ。」
生徒
「なるほど…!今日は具体例が多かったので、現場でどんな風に使えるのかイメージしやすかったです。これから自分で書くときも気をつけてみます!」
先生
「はい、その調子です。エラー処理を丁寧に書けると開発者としてのレベルがぐっと上がりますから、ぜひ実践しながら身につけていきましょう。」