はじめに
Goでのエラーハンドリングについて整理します。
大体把握できたと思いますので、メモっておきます。
エラーハンドリング例
コード例
// メソッド呼び出し
if err := executeTransaction(db); err != nil {
fmt.Printf("Transaction failed: %v\n", err)
} else {
fmt.Println("Transaction succeeded.")
}
func manageUser(tx *gorm.DB, checkUserID int, checkTargetUserID int) error {
var matchedUser User
result := tx.Where("user_id = ? AND target_user_id = ?", checkUserID, checkTargetUserID).First(&matchedUser)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// レコードが見つからなかった場合の処理
newUser := User{UserID: checkUserID, TargetUserID: checkTargetUserID, Name: "New User", Timestamp: time.Now()}
if err := tx.Create(&newUser).Error; err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
fmt.Println("No existing user found. Added new user:", newUser)
} else {
// その他のエラーの場合の処理
return fmt.Errorf("database error: %w", result.Error)
}
} else {
// エラーが発生しなかった場合(レコードが正常に見つかった場合)
if err := tx.Delete(&matchedUser).Error; err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
fmt.Println("Existing user found. Deleted user:", matchedUser)
}
return nil
}
エラーハンドリングの具体的な手法について整理して説明します。
トランザクションの実行箇所を例をあげます。
トランザクションの実行
executeTransaction
関数はトランザクション処理を行います。以下はその詳細な手順です。
トランザクションの開始
トランザクションはdb.Begin()
で開始されます。開始に失敗した場合、そのエラーを返します。
tx := db.Begin()
if tx.Error != nil {
return fmt.Errorf("failed to begin transaction: %w", tx.Error)
}
トランザクションの処理
tx.Where().First()
で特定のユーザーを検索します。ここでのエラーハンドリングは次のように分岐されます:
- レコードが見つからない場合(
gorm.ErrRecordNotFound
)、新しいユーザーを作成してデータベースに追加します。 - その他のデータベースエラーが発生した場合、エラーを返します。
- レコードが見つかった場合、該当ユーザーを削除します。
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// 新規ユーザー追加処理
} else {
return fmt.Errorf("database error: %w", result.Error)
}
} else {
// ユーザー削除処理
}
トランザクションの終了
全ての操作が無エラーで終了した場合、トランザクションはコミットされます。エラーが発生した場合は自動的にロールバックされます(defer
を使用)。
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
バージョンによるエラー処理の違い
result := tx.Where("user_id = ? AND target_user_id = ?", checkUserID, checkTargetUserID).First(&matchedUser)
if result.Error == gorm.ErrRecordNotFound {
newUser := User{UserID: checkUserID, TargetUserID: checkTargetUserID, Name: "New User", Timestamp: time.Now()}
if err := tx.Create(&newUser).Error; err != nil {
tx.Error = err // トランザクションエラーを設定
return fmt.Errorf("failed to create user: %w", err)
}
fmt.Println("No existing user found. Added new user:", newUser)
} else if result.Error != nil {
tx.Error = result.Error // トランザクションエラーを設定
return fmt.Errorf("failed to find user: %w", result.Error)
} else {
if err := tx.Delete(&matchedUser).Error; err != nil {
tx.Error = err // トランザクションエラーを設定
return fmt.Errorf("failed to delete user: %w", err)
}
fmt.Println("Existing user found. Deleted user:", matchedUser)
}
if result.Error == gorm.ErrRecordNotFound
の部分で gorm.ErrRecordNotFound
エラーをチェックしていますが、この方法はGormの古いバージョンで推奨されていたやり方です。
Gorm v1ではこのような直接的なエラーチェックが一般的でした。
しかし、Gorm v2以降ではエラーを処理する方法が変更されており、errors.Is
関数を使ってエラーの型を確認するのが推奨されています。
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// レコードが見つからなかった時の処理
}
もしGorm v2以降を使用している場合は、エラーチェックをこの方法に更新することをお勧めします。errors.Is
を使った現在のエラーチェック方法にコードが更新されていないということになります。
エラーをラップする
Go言語ではエラーハンドリングの一環として「エラーのラップ」が一般的に行われます。エラーをラップするというのは、発生したエラーに追加の情報を加えたり、エラーの文脈を明確にするために新しいエラーメッセージで元のエラーを包み込むことを指します。これにより、エラーがどこで何故発生したのかをより詳細に追跡しやすくなります。
Go 1.13以降では、fmt.Errorf
関数を使って %w
フォーマット指定子を用いることで、エラーを簡単にラップできます。これにより、errors.Is
や errors.As
などの関数を使用してエラーの原因を後から解析できるようになります。
たとえば、以下のようにエラーをラップしています:
if err := tx.Create(&newUser).Error; err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
このコードでは、tx.Create(&newUser).Error
で発生したエラー(err
)を fmt.Errorf
を使って新しいエラーメッセージに包み込み、元のエラーを %w
を用いて埋め込んでいます。これにより、エラーが上の階層に返されたときに、どのような操作でどんなエラーが発生したかが明確になりますし、元のエラーを損なうことなく、さらに詳細なエラーチェックが可能になります。
エラーハンドリングの方針
一般的なガイドラインを示します。
外部システムに依存する操作
- データベースアクセス: データベースのクエリやトランザクション処理は、接続の問題やデータの整合性の問題など、さまざまなエラーが発生する可能性があるため、適切なエラーハンドリングが必要です。
- ネットワーク通信: API呼び出しや外部サービスへのリクエストなど、ネットワークの状態や相手サーバーの応答によってエラーが発生することが多いため、エラーハンドリングを行う必要があります。
- ファイル入出力: ディスクの空き容量不足、アクセス権限の問題、ファイルの損傷など、さまざまな理由でエラーが発生する可能性があるため、注意が必要です。
ローカルで完結する操作
- スライスやマップなどのデータ構造の操作: これらの操作はメモリ内で行われるため、通常はエラーハンドリングを行う必要がありません。ただし、不正なインデックスアクセス(配列外アクセスなど)はパニックを引き起こすため、事前の範囲チェックが推奨されます。
- 単純な計算: 単純な算術計算や文字列操作など、外部の状態に依存しない操作では、エラーハンドリングは通常不要です。
例外的なケース
- パニックの使用: Goでは例外を使う代わりに、主に「パニック(panic)」を使用しますが、これはプログラムの正常なフローではなく、通常は回避すべき状況です。パニックは、回復不可能なエラーの場合に限って使用し、可能な限りエラーハンドリングで対応することが推奨されます。
Goのエラーハンドリングの特徴
Go言語のエラーハンドリングは他の多くのプログラミング言語といくつかの重要な点で異なりますが、基本的な原則としては共通しています。以下にGoのエラーハンドリングの特徴と他の言語との比較をまとめてみます。
Go言語のエラーハンドリングの特徴
-
明示的なエラーチェック: Goでは、エラーを返す関数は通常、戻り値として
error
型を返します。このエラーをチェックする責任は呼び出し側にあります。これは、例外を用いる言語(例えば、JavaやPython)とは異なり、プログラマがエラーを無視することが困難になっています。result, err := someFunction() if err != nil { // エラーハンドリング }
-
カスタムエラータイプ: Goでは
error
インターフェースを実装することでカスタムエラータイプを作成することができます。これにより、エラーの種類に基づいて異なるアクションを取ることが容易になります。type MyError struct { Msg string Code int } func (e *MyError) Error() string { return e.Msg }
-
パニックとリカバー: Goでは、パニック(プログラムの通常のフローを中断する重大なエラー)を発生させることができます。ただし、パニックは通常、回復不能なプログラムのエラーで使用され、リカバーを使用してこれをキャッチすることができます。これは他の言語の「try-catch」ブロックに似ていますが、使用は推奨されません。
func riskyFunction() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered from:", r) } }() panic("something went wrong") }
他の言語との比較
-
Java/C#: これらの言語では、例外をスローし、
try-catch
ブロックを使用して例外をキャッチします。これにより、エラーの原因となるコードとエラーのハンドリングコードが物理的に分離されます。 -
Python/JavaScript: これらの言語も例外を使用し、
try-except
(Python)やtry-catch
(JavaScript)を使用して例外をハンドリングします。
結論
エラーハンドリングの基本的な観点は多くの言語で共通しており、それはプログラムの信頼性を確保するために、予期しない状況やエラーを適切に処理することです。
その具体的な実装方法や哲学は言語によって異なります。
Goはその明示的なエラーチェックとパニックメカニズムによって、他の言語と一線を画しています。この方式は、エラーをより意識的に処理することを強制し、堅牢なエラーハンドリングを促進することを目的としています。
一連のコード例
トランザクション処理のみ記載していましたが、データの読み込み等の一連の完全なコード例を記載しておきます。
package main
import (
"errors"
"fmt"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
UserID int `gorm:"primaryKey;autoIncrement:false"`
TargetUserID int `gorm:"primaryKey;autoIncrement:false"`
Name string
Timestamp time.Time
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
fmt.Printf("Failed to connect to database: %v\n", err)
return
}
if err := db.AutoMigrate(&User{}); err != nil {
fmt.Printf("Failed to migrate database: %v\n", err)
return
}
if err := executeTransaction(db); err != nil {
fmt.Printf("Transaction failed: %v\n", err)
} else {
fmt.Println("Transaction succeeded.")
}
showUsers(db)
}
func executeTransaction(db *gorm.DB) error {
tx := db.Begin()
if tx.Error != nil {
return fmt.Errorf("failed to begin transaction: %w", tx.Error)
}
// トランザクションが失敗した場合にのみRollbackを実行
defer func() {
if tx.Error != nil {
tx.Rollback()
}
}()
checkUserID := 1
checkTargetUserID := 40
var matchedUser User
result := tx.Where("user_id = ? AND target_user_id = ?", checkUserID, checkTargetUserID).First(&matchedUser)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// レコードが見つからなかった場合の処理
newUser := User{UserID: checkUserID, TargetUserID: checkTargetUserID, Name: "New User", Timestamp: time.Now()}
// newUser := User{UserID: checkUserID, TargetUserID: checkTargetUserID, Name: strings.Repeat("a", 10000), Timestamp: time.Now()}
if err := tx.Create(&newUser).Error; err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
fmt.Println("No existing user found. Added new user:", newUser)
} else {
// その他のエラーの場合の処理
return fmt.Errorf("database error: %w", result.Error)
}
} else {
fmt.Println("Existing user found. Deleted user:", matchedUser)
// エラーが発生しなかった場合(レコードが正常に見つかった場合)
if err := tx.Delete(&matchedUser).Error; err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
// トランザクションのテスト用にエラーを返す
// return errors.New("manual trigger error for rollback test")
fmt.Println("Existing user found. Deleted user:", matchedUser)
}
// エラーがなければCommitを実行
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
func showUsers(db *gorm.DB) {
var users []User
if err := db.Find(&users).Error; err != nil {
fmt.Printf("Failed to list users: %v\n", err)
return
}
for _, user := range users {
fmt.Printf("UserID: %d, TargetUserID: %d, Name: %s, Timestamp: %s\n",
user.UserID, user.TargetUserID, user.Name, user.Timestamp)
}
}
gorm
ライブラリを用いてSQLiteデータベースに対して操作を行います。
1. データベース接続の確立
データベースとの接続はgorm.Open
を使って行います。ここでエラーが発生した場合、エラーメッセージを表示しプログラムを終了します。
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
fmt.Printf("Failed to connect to database: %v\n", err)
return
}
2. データベースマイグレーション
データベーススキーマのマイグレーション(ここではUser
テーブルの作成)を行います。マイグレーション中にエラーが発生した場合もエラーを表示し、プログラムを終了します。
if err := db.AutoMigrate(&User{}); err != nil {
fmt.Printf("Failed to migrate database: %v\n", err)
return
}
3. トランザクションの実行
最初の例に記載。
4. ユーザーデータの表示
最後に、データベース内の全ユーザーのデータを表示します。エラーがあれば、そのエラーメッセージを表示します。
func showUsers(db *gorm.DB) {
var users []User
if err := db.Find(&users).Error; err != nil {
fmt.Printf("Failed to list users: %v\n", err)
return
}
for _, user := range users {
fmt.Printf("UserID: %d, TargetUserID: %d, Name: %s, Timestamp: %s\n",
user.UserID, user.TargetUserID, user.Name, user.Timestamp)
}
}
参考記事