Go のエラーハンドリングのベストプラクティス
Go のエラーハンドリングはシンプルだが、適切に行わないとコードが読みにくくなったり、デバッグが困難になったりする。ここでは実践的なベストプラクティスを紹介する。
エラーは必ずチェックする
Go では未使用の変数があるとコンパイルエラーになるが、エラーを _ で無視すると警告が出ない。
// 悪い例:エラーを無視している
data, _ := ioutil.ReadFile("config.json")
// 良い例:エラーをチェックしている
data, err := ioutil.ReadFile("config.json")
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}エラーを無視すると、問題が発生したとき原因の特定が非常に困難になる。無視してよいケースは極めて稀だ。
エラーメッセージにコンテキストを追加する
エラーをそのまま返すのではなく、どこで何をしていたかを追加すると、デバッグが容易になる。
“file not found” だけでは、どのファイルで何をしていたかわからない。
“failed to load user config /etc/app/config.json: file not found” なら状況が明確。
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse config file %s: %w", path, err)
}
return &cfg, nil
}エラーチェックは早期リターンで
条件分岐を深くネストさせず、エラーがあれば即座にリターンする。
// 悪い例:ネストが深い
func process(id int) error {
user, err := getUser(id)
if err == nil {
orders, err := getOrders(user.ID)
if err == nil {
// 処理...
} else {
return err
}
} else {
return err
}
return nil
}
// 良い例:早期リターン
func process(id int) error {
user, err := getUser(id)
if err != nil {
return fmt.Errorf("get user: %w", err)
}
orders, err := getOrders(user.ID)
if err != nil {
return fmt.Errorf("get orders: %w", err)
}
// 処理...
return nil
}早期リターンにより、正常系のコードがネストなしで読める。
センチネルエラーの定義
パッケージ外から判定したいエラーは、パッケージレベルで定義する。
package user
var (
ErrNotFound = errors.New("user not found")
ErrAlreadyExists = errors.New("user already exists")
ErrInvalidInput = errors.New("invalid input")
)呼び出し側は errors.Is で判定できる。
user, err := user.Find(id)
if errors.Is(err, user.ErrNotFound) {
// 404 を返す
}エラーをログに記録するタイミング
エラーを処理(ログ記録やユーザーへの通知)するのは、エラーチェーンの最上位で行う。
エラーをラップして返す。ログには記録しない。
エラーをログに記録し、適切なレスポンスを返す。
下位層でもログを出すと、同じエラーが複数回ログに出力されてしまう。
// ハンドラ層でログとレスポンス
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.FindUser(id)
if err != nil {
log.Printf("failed to get user %d: %v", id, err)
if errors.Is(err, ErrNotFound) {
http.Error(w, "User not found", 404)
} else {
http.Error(w, "Internal error", 500)
}
return
}
// ...
}defer でのエラー処理
defer 内でエラーが発生した場合、名前付き戻り値を使って返すことができる。
func writeFile(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
_, err = f.Write(data)
return err
}Close のエラーを握りつぶさず、かつ Write のエラーを上書きしないようにしている。エラーハンドリングの基本は「エラーを見逃さない」ことだ。適切にコンテキストを追加し、一貫したパターンで処理することで、保守しやすいコードになる。