技術Tips
Goのエラーハンドリングパターン完全ガイド
Goにおけるエラーハンドリングの基本から、実践的なパターン、Go 1.13以降の新機能まで詳しく解説します。
佐藤 裕介
CTO / シニアソフトウェアエンジニア
Go エラーハンドリング バックエンド 設計パターン
はじめに
Goのエラーハンドリングは、他の言語のtry-catch文と異なり、明示的にエラーを返して処理します。一見冗長に見えますが、適切なパターンを使えば、堅牢で保守性の高いコードを書くことができます。
この記事では、Goのエラーハンドリングの基本から、実践的なパターン、Go 1.13以降の新機能まで詳しく解説します。
基本: errorインターフェース
Goのerrorは単純なインターフェースです。
type error interface {
Error() string
}
エラーの生成
import "errors"
// シンプルなエラー
err := errors.New("something went wrong")
// フォーマット付きエラー
err := fmt.Errorf("failed to process user %s", userID)
エラーチェックの基本パターン
func readFile(filename string) ([]byte, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return data, nil
}
カスタムエラー型の定義
構造体によるカスタムエラー
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on field %s: %s", e.Field, e.Message)
}
// 使用例
func validateEmail(email string) error {
if !strings.Contains(email, "@") {
return &ValidationError{
Field: "email",
Message: "must contain @ symbol",
}
}
return nil
}
エラーの型アサーション
err := validateEmail("invalid-email")
if err != nil {
if ve, ok := err.(*ValidationError); ok {
fmt.Printf("Validation failed on %s: %s\n", ve.Field, ve.Message)
} else {
fmt.Printf("Unknown error: %v\n", err)
}
}
Go 1.13以降のエラーハンドリング
エラーのラップ
fmt.Errorfの%w動詞を使ってエラーをラップできます。
func processUser(id string) error {
user, err := fetchUser(id)
if err != nil {
return fmt.Errorf("failed to process user %s: %w", id, err)
}
// 処理...
return nil
}
errors.Is: エラーの比較
var ErrNotFound = errors.New("not found")
func getUser(id string) (*User, error) {
// ...
if userNotExists {
return nil, ErrNotFound
}
return user, nil
}
// 使用例
user, err := getUser("123")
if errors.Is(err, ErrNotFound) {
// NotFoundエラーの処理
return nil, fmt.Errorf("user not found: %w", err)
}
errors.As: エラー型の取得
type TemporaryError interface {
error
Temporary() bool
}
func handleError(err error) {
var tempErr TemporaryError
if errors.As(err, &tempErr) && tempErr.Temporary() {
// 一時的なエラーの場合はリトライ
retry()
} else {
// それ以外は諦める
logError(err)
}
}
実践パターン1: Sentinel Error
定義済みのエラー値を使って、特定のエラー状態を表現します。
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
func getUserByID(id string) (*User, error) {
if id == "" {
return nil, ErrInvalidInput
}
user, found := db.Find(id)
if !found {
return nil, ErrNotFound
}
return user, nil
}
// エラーチェック
user, err := getUserByID("123")
if err != nil {
switch {
case errors.Is(err, ErrNotFound):
return http.StatusNotFound
case errors.Is(err, ErrUnauthorized):
return http.StatusUnauthorized
default:
return http.StatusInternalServerError
}
}
実践パターン2: カスタムエラー型
より詳細な情報を持つエラー型を定義します。
type DBError struct {
Op string // 操作名
Table string // テーブル名
Err error // 元のエラー
}
func (e *DBError) Error() string {
return fmt.Sprintf("db error: op=%s table=%s: %v", e.Op, e.Table, e.Err)
}
func (e *DBError) Unwrap() error {
return e.Err
}
// 使用例
func insertUser(user *User) error {
err := db.Insert("users", user)
if err != nil {
return &DBError{
Op: "insert",
Table: "users",
Err: err,
}
}
return nil
}
実践パターン3: 複数のエラーを集約
type MultiError struct {
Errors []error
}
func (m *MultiError) Error() string {
var messages []string
for _, err := range m.Errors {
messages = append(messages, err.Error())
}
return strings.Join(messages, "; ")
}
func (m *MultiError) Add(err error) {
if err != nil {
m.Errors = append(m.Errors, err)
}
}
func (m *MultiError) HasErrors() bool {
return len(m.Errors) > 0
}
// 使用例
func validateUser(user *User) error {
var merr MultiError
if user.Name == "" {
merr.Add(errors.New("name is required"))
}
if user.Email == "" {
merr.Add(errors.New("email is required"))
}
if user.Age < 0 {
merr.Add(errors.New("age must be positive"))
}
if merr.HasErrors() {
return &merr
}
return nil
}
実践パターン4: パニックとリカバリー
通常のエラーハンドリングではなく、パニックを使うべき場面もあります。
// パニックを起こすべき状況
func mustParseConfig(data []byte) *Config {
config, err := parseConfig(data)
if err != nil {
// 設定ファイルの解析エラーは回復不可能
panic(fmt.Sprintf("config parse error: %v", err))
}
return config
}
// リカバリーによるエラーハンドリング
func safeHandler(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic recovered: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
// 実際のハンドラ処理
handleRequest(w, r)
}
実践パターン5: コンテキストとエラー
context.Contextを使ったタイムアウトとキャンセルの処理。
func fetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
// タイムアウトやキャンセルを検出
if errors.Is(err, context.DeadlineExceeded) {
return nil, fmt.Errorf("request timeout: %w", err)
}
if errors.Is(err, context.Canceled) {
return nil, fmt.Errorf("request canceled: %w", err)
}
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
エラーハンドリングの推奨パターン
1. エラーを適切に処理する
// ❌ 悪い例
data, _ := os.ReadFile("config.json")
// ✅ 良い例
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
2. エラーにコンテキストを追加
// ❌ 悪い例
if err != nil {
return err
}
// ✅ 良い例
if err != nil {
return fmt.Errorf("failed to process user %s: %w", userID, err)
}
3. エラーの内容を変更しない
// ❌ 悪い例
if err != nil {
return errors.New(err.Error()) // 元のエラー情報が失われる
}
// ✅ 良い例
if err != nil {
return fmt.Errorf("processing failed: %w", err)
}
4. センシティブな情報を含めない
// ❌ 悪い例
return fmt.Errorf("authentication failed for password %s", password)
// ✅ 良い例
return errors.New("authentication failed")
まとめ
Goのエラーハンドリングは明示的で冗長に見えますが、以下のメリットがあります:
- エラー処理のフローが明確
- エラーが発生する箇所が一目瞭然
- 型安全なエラーハンドリング
Go 1.13以降のerrors.Is、errors.As、エラーのラップ機能を活用することで、より堅牢なエラーハンドリングが可能になります。
参考リンク
著者について
佐藤 裕介
CTO / シニアソフトウェアエンジニア