Go言語のエラーハンドリング:実践的なパターンと最適解
はじめに
Go言語のエラーハンドリングは、他の多くのプログラミング言語とは異なるアプローチを取っています。例外(Exception)を使わず、エラーを明示的に戻り値として返すという設計思想は、最初は冗長に感じるかもしれません。しかし、この方法には多くの利点があり、適切に使いこなすことでより堅牢なコードを書くことができます。
本記事では、Go言語におけるエラーハンドリングの基本から実践的なパターンまでを解説します。
基本的なエラーハンドリング
Go言語では、関数がエラーを返す可能性がある場合、慣習的に最後の戻り値としてerror型を返します。
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Println("結果:", result)
}
この例では、ゼロ除算が発生する可能性がある場合にエラーを返しています。呼び出し側では必ずerr != nilをチェックすることが必要です。
カスタムエラーの作成
より詳細なエラー情報を提供したい場合、カスタムエラー型を定義することができます。
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("バリデーションエラー [%s]: %s", e.Field, e.Message)
}
func validateUser(name string, age int) error {
if name == "" {
return &ValidationError{
Field: "name",
Message: "名前は必須です",
}
}
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: "年齢が不正です",
}
}
return nil
}
カスタムエラー型を使うことで、エラーの種類を型で判別でき、より柔軟なエラーハンドリングが可能になります。
エラーのラップと追跡
Go 1.13以降では、fmt.Errorfの%w動詞を使ってエラーをラップできるようになりました。これにより、エラーが発生した箇所のコンテキスト情報を保持しながら、元のエラーも保持できます。
func readConfig(filename string) (*Config, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("設定ファイルの読み込みに失敗: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("設定ファイルのパースに失敗 (%s): %w", filename, err)
}
return &config, nil
}
ラップされたエラーは、errors.Is()やerrors.As()を使って元のエラーを確認できます。
config, err := readConfig("config.json")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println("設定ファイルが存在しません")
} else {
fmt.Println("エラー:", err)
}
}
センチネルエラーの使用
特定のエラー条件を表現するために、あらかじめ定義されたエラー値(センチネルエラー)を使用するパターンも一般的です。
var (
ErrUserNotFound = errors.New("ユーザーが見つかりません")
ErrInvalidPassword = errors.New("パスワードが正しくありません")
ErrAccountLocked = errors.New("アカウントがロックされています")
)
func login(username, password string) error {
user, err := findUser(username)
if err != nil {
return ErrUserNotFound
}
if user.IsLocked {
return ErrAccountLocked
}
if !user.ValidatePassword(password) {
return ErrInvalidPassword
}
return nil
}
センチネルエラーを使うことで、エラーの種類を明確に区別でき、呼び出し側で適切な処理を行うことができます。
エラーハンドリングのベストプラクティス
1. エラーを無視しない
// 悪い例
file, _ := os.Open("config.json")
// 良い例
file, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("ファイルを開けません: %w", err)
}
defer file.Close()
2. エラーメッセージに大文字や句点を使わない
Go言語の慣習として、エラーメッセージは小文字で始め、句点で終わらせません。これは、エラーメッセージが他のメッセージと組み合わせて使用されることを想定しているためです。
// 悪い例
errors.New("ファイルが見つかりません。")
// 良い例
errors.New("ファイルが見つかりません")
3. コンテキスト情報を追加する
エラーが発生した際には、デバッグに役立つコンテキスト情報を追加します。
func processFile(filename string) error {
data, err := readFile(filename)
if err != nil {
return fmt.Errorf("ファイル処理エラー (file: %s): %w", filename, err)
}
// 処理...
return nil
}
4. パニックは本当に回復不可能な場合のみ
panicは例外的な状況(プログラムが継続できない場合)にのみ使用し、通常のエラーハンドリングにはerrorを使用します。
// パニックを使うべき例(初期化時の致命的エラー)
func init() {
if !systemRequirementsMet() {
panic("システム要件を満たしていません")
}
}
// 通常のエラーハンドリングを使うべき例
func createUser(name string) (*User, error) {
if name == "" {
return nil, errors.New("名前は必須です")
}
return &User{Name: name}, nil
}
まとめ
Go言語のエラーハンドリングは、明示的で予測可能なコードを書くことを促進します。以下のポイントを押さえることで、堅牢で保守性の高いコードを書くことができます:
- エラーを戻り値として明示的に返す
- カスタムエラー型で詳細な情報を提供する
- エラーをラップしてコンテキストを保持する
- センチネルエラーで特定のエラー条件を表現する
- エラーを無視せず、適切に処理する