他言語からGo言語に入った際に、最初に戸惑いやすいのがエラー処理の仕組みです。
JavaやPython、TypeScriptなどの多くにある try-catch-finally による「例外」がなく、Goでは if err != nil を何度も書くスタイルが採用されています。
最近、Go言語を再び触る機会が増えたので、この記事では、なぜGoでは例外ではなく値としてエラーを扱うのかという思想的な背景から、if err != nil だらけでロジックが複雑化するのを防ぐための実践的なコツまでをまとめました。
1. Go言語のエラー処理の仕組み(例外との違い)
Go言語には一般的な try-catch が存在しません。(panic / recover という仕組みは存在しますが、これは復旧不能な致命的状態にのみ使うもので、通常の例外処理としては使用しません)。
Goの error の正体
Goのエラーは特別な言語構文ではなく、組み込みの非常にシンプルな interface です。
type error interface {
Error() string
}
関数は「正常な結果」と「error(失敗時はエラーオブジェクト、正常時は nil)」を**マルチリターン(多重戻り値)**で返します。
val, err := rdb.Get(ctx, "key").Result()
if err != nil {
// エラーハンドリング
}
2. なぜ try-catch ではなく「値としてのエラー」なのか?
Goの設計者(Rob PikeやKen Thompsonら)は、例外処理が持つ以下の問題を避けるために**「エラーは単なる値(Errors are values)」**という設計を採用しました。
-
暗黙的な制御フローの隠蔽の防止
tryブロック内のどの行で例外がスローされるかが視覚的に分からず、ジャンプ先のcatchまで処理を追うのが難しくなるのを防ぎます。 -
関数のシグネチャの明瞭さ
関数の戻り値を見れば「この関数が失敗する可能性があるか」が一目で分かります。 -
キャッチ漏れによるクラッシュの防止
呼び出し側が明示的にエラーを受け取るため、ハンドリングの忘れを防ぎやすくなります。
3. ロジックを複雑化させない5つのコツ
Goでコードを書いていると if err != nil が大量に登場し、ロジックが埋もれてしまいがちです。これを整理し、美しくメンテナンスしやすいコードにするためのコツを紹介します。
コツ①:Happy Path(正常系)を左側に揃える(Early Return)
エラーが発生したら**即座に return して関数を抜ける(ガード節)**スタイルを徹底します。
❌ ネストが深くなりやすいコード
val, err := rdb.Get(ctx, key).Result()
if err == nil {
// 正常系の長い処理が続く...
process(val)
} else {
log.Println(err)
}
⭕️ GoのIdiomaticなコード(Happy Pathがインデント0の位置に保たれる)
val, err := rdb.Get(ctx, key).Result()
if err != nil {
return fmt.Errorf("redis get failed: %w", err) // 早めに抜ける
}
// ここからはエラーがない安全な状態。インデントが深くならない!
process(val)
コツ②:エラーラッピング(fmt.Errorf("%w"))とコンテキスト付与
下層の関数からエラーが返ってきた時、そのまま上層に投げると「どこで何が原因で落ちたか」が追えなくなります。
fmt.Errorf("%w", err) を使って「何を行おうとして失敗したか」の**文脈(コンテキスト)**を付与して呼び出し元へ返します。
func getUserProfile(userID string) (*Profile, error) {
user, err := findUser(userID)
if err != nil {
// %w を使うことで元のエラーを保持(ラップ)しつつ、文脈を追加できる
return nil, fmt.Errorf("failed to find user for ID %s: %w", userID, err)
}
return user.Profile, nil
}
呼び出し側では errors.Is や errors.As を使って、ラップされたエラーから元のエラーを判定・抽出できます。
-
errors.Is(err, target): 特定のエラー値(Redisのredis.Nilなど)と比較する -
errors.As(err, &targetStruct): 特定のカスタムエラー構造体にキャストする
val, err := rdb.Get(ctx, "missing_key").Result()
if errors.Is(err, redis.Nil) {
fmt.Println("キーが存在しません(センチネルエラーの判定)")
}
コツ③:「Errors are values」の活用(エラー状態を保持する構造体)
連続するステップで毎回 if err != nil を書くとコードが冗長になります。
Go公式ブログで紹介されている 「エラー状態を構造体内部に保持するパターン」 を使うと、コードをスッキリさせることができます。
❌ 毎回 if err != nil を書く例
_, err := w.Write(buf1)
if err != nil { return err }
_, err = w.Write(buf2)
if err != nil { return err }
_, err = w.Write(buf3)
if err != nil { return err }
⭕️ エラーを内部保持するラッパー構造体
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return // すでにエラーが発生していれば何もしない
}
_, ew.err = ew.w.Write(buf)
}
// 呼び出し側:何回書き込んでも最後に1回チェックするだけで済む
ew := &errWriter{w: writer}
ew.write(buf1)
ew.write(buf2)
ew.write(buf3)
if ew.err != nil {
return ew.err // どこかで発生したエラーをここで一括処理
}
※標準ライブラリの bufio.Scanner や zip.Writer などでも採用されているテクニックです。
コツ④:ヘルパー関数やミドルウェアへのエラー処理切り出し
WebサーバーのHTTPハンドラーなどで、共通のレスポンス処理とエラーログ出力をクロージャやミドルウェアにまとめます。
// エラーを返す独自のハンドラー型
type AppHandler func(w http.ResponseWriter, r *http.Request) error
// 共通のエラーハンドリングを行うラッパー関数
func HandleError(h AppHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
// ここで一括ログ出力やエラーレスポンス返却を行う
log.Printf("[ERROR] %s: %v", r.URL.Path, err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
}
コツ⑤:panic / recover は「真の異常事態」以外で使わない
Goにも panic と recover という例外に近い仕組みがありますが、通常の業務エラー処理で panic を使うのはアンチパターンです。
-
panicの使い所:- アプリ起動時に必須の設定ファイルが読み込めず、プロセスを継続できないレベルの致命的エラー。
- ライブラリの初期化ロジック(
MustCompileなど)。
-
errorの使い所:- ネットワークエラー、DB参照失敗、入力バリデーション違反など、想定されるすべての失敗ケース。
4. まとめ
| ポイント | 説明 |
|---|---|
| Goのエラーの思想 | エラーは「例外」ではなく「単なる値(インターフェース)」。 |
| ガード節(Early Return) | エラーならすぐ return して、正常系コードのインデントを浅く保つ。 |
| エラーラッピング |
fmt.Errorf("%w", err) でどこで起きたかのコンテキストを付け加える。 |
| Errors are Values | 連続した処理では構造体にエラーを持たせ、最後にまとめてチェックする手法を活用する。 |
| panicの自制 | 復旧不能な致命的状態以外では panic ではなく error を返す。 |
Goのエラー処理は「冗長」と言われることも多いですが、「コードを見ただけでどこで失敗する可能性があり、どうハンドリングされているかが完全に明瞭である」 という大きなメリットを持っています。
ぜひ適切なパターンを活用して、可読性の高いGoコードを書いてみてください!