エラー処理
レッスン10:エラー処理
現実世界のアナロジー
レストランで料理を注文する場面を想像してください。ウェイターがこう言います:「申し訳ありません、本日この料理は売り切れです。」 — これは予想される状況です。別の料理を注文すればいいだけですし、生活は続きます。これがGoの error です。
しかし、厨房が突然火災を起こし、全員が直ちに避難しなければならない — これは予期しない災害です。これがGoの panic です。
Goのエラー処理哲学は明確です:予想される問題は优雅に処理し、予期しない問題のときだけクラッシュします。 他の言語がすべての例外を try/catch で混ぜるのとは異なり、Goでは各エラーを明示的かつ個別に処理する必要があります。
コアコンセプト
| コンセプト | 説明 |
|---|---|
error インターフェース |
Goの組み込みエラータイプ。メソッドは Error() string のみ |
| 複数の戻り値 | Goの関数は通常、最後の戻り値としてエラーを返す |
errors.New |
簡単なテキストエラーを作成 |
fmt.Errorf |
フォーマットされたエラーを作成(%w でラップ可能) |
errors.Is |
エラーチェーンに特定のエラーが含まれているかチェック |
errors.As |
エラーチェーンから特定のエラータイプを抽出 |
panic |
ランタイムパニックを引き起こし、プログラムをクラッシュさせる |
recover |
defer で panic をキャッチし、プログラムのクラッシュを防止 |
| エラーラッピング | %w またはカスタム型で基礎エラーをラップし、コンテキストを保持 |
エラー処理フローチャート
関数がエラーを返す
│
▼
err != nil ?
┌────┴────┐
│ はい │ いいえ
▼ ▼
エラーを処理 続行
│
├─ 回復可能 → ログ / デフォルト値を返す / リトライ
├─ 報告が必要 → ラップして上位に渡す
└─ 回復不可能 → panic
基本構文と使い方
1. error インターフェース
error はGoの組み込みインターフェース型で、非常に簡潔に定義されています。
// error インターフェースの定義(組み込み、インポート不要)
type error interface {
Error() string
}
Error() string メソッドを実装するすべての型が error です。
2. エラーの作成
package main
import (
"errors"
"fmt"
)
func main() {
// 方法1: errors.New — 簡単なテキストエラーを作成
err1 := errors.New("ファイルが見つかりません")
// 方法2: fmt.Errorf — フォーマットされたエラーを作成
filename := "config.yaml"
err2 := fmt.Errorf("ファイル %s の読み込みに失敗しました", filename)
// 方法3: fmt.Errorf + %w — 基礎エラーをラップ(推奨)
baseErr := errors.New("権限がありません")
err3 := fmt.Errorf("ログの書き込みができません: %w", baseErr)
fmt.Println(err1) // ファイルが見つかりません
fmt.Println(err2) // ファイル config.yaml の読み込みに失敗しました
fmt.Println(err3) // ログの書き込みができません: 権限がありません
}
3. エラーのチェック
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("レコードが見つかりません")
func findUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return "Alice", nil
}
func main() {
name, err := findUser(0)
if err != nil {
// errors.Is はエラーチェーンにターゲットエラーが含まれているかチェック
if errors.Is(err, ErrNotFound) {
fmt.Println("ユーザーが見つかりません。IDを確認してください")
} else {
fmt.Println("不明なエラー:", err)
}
return
}
fmt.Println("ユーザーを見つけました:", name)
}
4. 特定のエラータイプの抽出
package main
import (
"errors"
"fmt"
)
// カスタムエラータイプ
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("バリデーションエラー [%s]: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: "年齢は0から150の間でなければなりません",
}
}
return nil
}
func main() {
err := validateAge(200)
if err != nil {
// errors.As はエラーチェーンから特定の型を抽出
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("フィールド: %s、理由: %s\n", ve.Field, ve.Message)
} else {
fmt.Println("その他のエラー:", err)
}
}
}
5. panic と recover
package main
import "fmt"
// safeDivide は recover を使って panic をキャッチする
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panicをキャッチしました: %v", r)
}
}()
// ゼロ除算は panic を引き起こす
return a / b, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("エラー:", err) // エラー: panicをキャッチしました: runtime error: integer divide by zero
return
}
fmt.Println("結果:", result)
}
error を返して処理すべきです。panic を使用するのは、プログラムが本当に続行不可能な場合のみ(初期化エラー、回復不可能なロジックエラーなど)です。
fmt.Errorf でエラーをラップするときは、常に %v ではなく %w を使用してください。%w は元のエラーを保持し、errors.Is と errors.As が正しく動作するようにします。
サンプルコード
例:基本的なエラー処理(難易度⭐)
簡単な設定ファイルリーダーをシミュレートし、最も基本的なエラーの返却とチェックパターンを実演します。
package main
import (
"errors"
"fmt"
"os"
)
// パッケージレベルのセンチネルエラーを定義
var (
ErrFileNotFound = errors.New("設定ファイルが見つかりません")
ErrFileEmpty = errors.New("設定ファイルが空です")
ErrInvalidFormat = errors.New("設定ファイルの形式が無効です")
)
// Config はアプリケーション設定を表す
type Config struct {
Host string
Port int
}
// LoadConfig はファイルから設定を読み込む(シミュレーション)
func LoadConfig(path string) (*Config, error) {
// ファイルが見つからない場合をシミュレート
if path == "" {
return nil, ErrFileNotFound
}
// ファイル読み込みをシミュレート
data, err := os.ReadFile(path)
if err != nil {
// 基礎エラーをラップし、元の情報を保持
return nil, fmt.Errorf("設定ファイル %s の読み込み: %w", path, err)
}
// ファイルが空かチェック
if len(data) == 0 {
return nil, ErrFileEmpty
}
// 設定の解析をシミュレート
return &Config{
Host: "localhost",
Port: 8080,
}, nil
}
func main() {
// 設定の読み込みを試行
config, err := LoadConfig("app.conf")
if err != nil {
// errors.Is で特定のエラータイプを判定
switch {
case errors.Is(err, ErrFileNotFound):
fmt.Println("エラー: 設定ファイルのパスを指定してください")
case errors.Is(err, ErrFileEmpty):
fmt.Println("エラー: 設定ファイルが空です。内容を確認してください")
case errors.Is(err, ErrInvalidFormat):
fmt.Println("エラー: 設定ファイルの形式が無効です")
default:
fmt.Println("エラー:", err)
}
return
}
fmt.Printf("設定の読み込み成功: %s:%d\n", config.Host, config.Port)
}
出力(ファイルが存在しない場合):
エラー: 設定ファイルのパスを指定してください
例:エラーラッピングとチェーンチェック(難易度⭐⭐)
多層の関数呼び出しにおけるエラーのラッピング、伝播、チェーンチェックを実演します。
package main
import (
"errors"
"fmt"
)
// ビジネスエラーを定義
var (
ErrUserNotFound = errors.New("ユーザーが見つかりません")
ErrPermission = errors.New("権限が不足しています")
)
// データベース層のエラーを表す
type DatabaseError struct {
Operation string
Table string
Err error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("データベースエラー [%s.%s]: %e", e.Table, e.Operation, e.Err)
}
func (e *DatabaseError) Unwrap() error {
return e.Err
}
// UserRepository はユーザーリポジトリ
type UserRepository struct {
users map[int]string
}
// FindByID はIDでユーザーを検索する
func (r *UserRepository) FindByID(id int) (string, error) {
name, exists := r.users[id]
if !exists {
// データベース層のエラーとしてラップ
return "", &DatabaseError{
Operation: "SELECT",
Table: "users",
Err: ErrUserNotFound,
}
}
return name, nil
}
// Service はビジネスサービス層
type Service struct {
repo *UserRepository
}
// GetUser はユーザー情報を取得する
func (s *Service) GetUser(id int, requireAdmin bool) (string, error) {
name, err := s.repo.FindByID(id)
if err != nil {
// 上位にラップし、ビジネスコンテキストを追加
return "", fmt.Errorf("ユーザー情報の取得 (id=%d): %w", id, err)
}
if requireAdmin && name != "admin" {
return "", fmt.Errorf("ユーザー %s: %w", name, ErrPermission)
}
return name, nil
}
func main() {
repo := &UserRepository{
users: map[int]string{
1: "admin",
2: "Alice",
},
}
svc := &Service{repo: repo}
// テストシナリオ
testCases := []struct {
name string
id int
requireAdmin bool
}{
{"管理者を検索", 1, false},
{"一般ユーザーを検索", 2, false},
{"存在しないユーザーを検索", 99, false},
{"一般ユーザーが管理機能にアクセス", 2, true},
}
for _, tc := range testCases {
fmt.Printf("--- %s ---\n", tc.name)
user, err := svc.GetUser(tc.id, tc.requireAdmin)
if err != nil {
fmt.Printf(" 失敗: %v\n", err)
// errors.Is はエラーチェーンを辿って根本原因を見つけられる
if errors.Is(err, ErrUserNotFound) {
fmt.Println(" 対応: ユーザーにIDの確認を促す")
} else if errors.Is(err, ErrPermission) {
fmt.Println(" 対応: ユーザーに管理者への連絡を促す")
}
// errors.As は中間層から特定のエラータイプを抽出できる
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
fmt.Printf(" DB詳細: テーブル=%s、操作=%s\n", dbErr.Table, dbErr.Operation)
}
} else {
fmt.Printf(" 成功: ユーザー %s\n", user)
}
fmt.Println()
}
}
出力:
--- 管理者を検索 ---
成功: ユーザー admin
--- 一般ユーザーを検索 ---
成功: ユーザー Alice
--- 存在しないユーザーを検索 ---
失敗: ユーザー情報の取得 (id=99): データベースエラー [users.SELECT]: ユーザーが見つかりません
対応: ユーザーにIDの確認を促す
DB詳細: テーブル=users、操作=SELECT
--- 一般ユーザーが管理機能にアクセス ---
失敗: ユーザー Alice: 権限が不足しています
対応: ユーザーに管理者への連絡を促す
例:カスタムエラータイプとrecoverの実践(難易度⭐⭐⭐)
カスタムエラータイプ、エラーコード、パニック回復ミドルウェアを備えた完全なHTTPリクエストハンドラを実装します。
package main
import (
"errors"
"fmt"
"runtime/debug"
)
// ==================== カスタムエラーシステム ====================
// AppError はアプリケーションレベルのエラーで、エラーコードとコンテキストを持つ
type AppError struct {
Code int // ビジネスエラーコード
Message string // ユーザーフレンドリーなメッセージ
Detail string // 開発者向けデバッグ情報
Cause error // 基礎原因
Context map[string]string // 追加コンテキスト
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Cause
}
// Is はカスタムエラー比較ロジックを実装
func (e *AppError) Is(target error) bool {
t, ok := target.(*AppError)
if !ok {
return false
}
return e.Code == t.Code
}
// NewAppError は新しいアプリケーションエラーを作成
func NewAppError(code int, message string) *AppError {
return &AppError{
Code: code,
Message: message,
Context: make(map[string]string),
}
}
// WithCause は基礎原因を設定
func (e *AppError) WithCause(err error) *AppError {
e.Cause = err
return e
}
// WithDetail はデバッグ詳細を設定
func (e *AppError) WithDetail(detail string) *AppError {
e.Detail = detail
return e
}
// WithContext はコンテキスト情報を追加
func (e *AppError) WithContext(key, value string) *AppError {
e.Context[key] = value
return e
}
// 事前定義されたエラーコード
var (
ErrBadRequest = NewAppError(400, "不正なリクエスト")
ErrUnauthorized = NewAppError(401, "認証されていません")
ErrForbidden = NewAppError(403, "アクセスが禁止されています")
ErrNotFound = NewAppError(404, "リソースが見つかりません")
ErrInternal = NewAppError(500, "内部サーバーエラー")
)
// ==================== ビジネス層のシミュレーション ====================
// Request はHTTPリクエストをシミュレート
type Request struct {
UserID int
Path string
IsAdmin bool
}
// Response はHTTPレスポンスをシミュレート
type Response struct {
StatusCode int
Body string
}
// validateRequest はリクエストパラメータを検証する
func validateRequest(req *Request) error {
if req.UserID <= 0 {
return ErrBadRequest.
WithDetail("user_id は正の整数でなければなりません").
WithContext("user_id", fmt.Sprintf("%d", req.UserID))
}
if req.Path == "" {
return ErrBadRequest.
WithDetail("path は空にできません")
}
return nil
}
// authenticate は認証
func authenticate(req *Request) error {
if req.UserID == 0 {
return ErrUnauthorized.WithDetail("認証資格情報がありません")
}
return nil
}
// authorize は認可
func authorize(req *Request) error {
if !req.IsAdmin {
return ErrForbidden.
WithDetail("管理者権限が必要です").
WithContext("user_id", fmt.Sprintf("%d", req.UserID))
}
return nil
}
// findResource はリソースを検索する(panic を引き起こす可能性がある)
func findResource(path string) (string, error) {
// panic を引き起こすバグをシミュレート
if path == "/crash" {
var p *int
*p = 1 // nilポインタのデリファレンス、panic を引き起こす
}
resources := map[string]string{
"/users": "ユーザーリスト",
"/profile": "ユーザープロフィール",
}
data, exists := resources[path]
if !exists {
return "", ErrNotFound.
WithDetail(fmt.Sprintf("パス %s のリソースが見つかりません", path))
}
return data, nil
}
// handleRequest はリクエストを処理する(ビジネスロジック)
func handleRequest(req *Request) (*Response, error) {
// 1. パラメータを検証
if err := validateRequest(req); err != nil {
return nil, err
}
// 2. 認証
if err := authenticate(req); err != nil {
return nil, err
}
// 3. 認可(管理パスのみ)
if req.Path == "/admin" {
if err := authorize(req); err != nil {
return nil, err
}
}
// 4. リソースを検索
data, err := findResource(req.Path)
if err != nil {
return nil, err
}
return &Response{
StatusCode: 200,
Body: data,
}, nil
}
// ==================== 回復ミドルウェア ====================
// RecoveryMiddleware はpanic回復ミドルウェア
func RecoveryMiddleware(handler func(*Request) (*Response, error)) func(*Request) (*Response, error) {
return func(req *Request) (resp *Response, err error) {
defer func() {
if r := recover(); r != nil {
// panicのスタックトレースをログに記録
stack := string(debug.Stack())
fmt.Printf("[PANIC] %v\nスタック:\n%s\n", r, stack)
// クラッシュの代わりに内部エラーを返す
err = ErrInternal.
WithDetail(fmt.Sprintf("panic: %v", r)).
WithCause(fmt.Errorf("panic: %v", r))
}
}()
return handler(req)
}
}
// ==================== エラーのフォーマット ====================
// formatError はエラーをフレンドリーなレスポンスにフォーマットする
func formatError(err error) *Response {
var appErr *AppError
if errors.As(err, &appErr) {
msg := fmt.Sprintf("エラー [%d]: %s", appErr.Code, appErr.Message)
if appErr.Detail != "" {
msg += fmt.Sprintf("\n 詳細: %s", appErr.Detail)
}
if len(appErr.Context) > 0 {
msg += "\n コンテキスト:"
for k, v := range appErr.Context {
msg += fmt.Sprintf("\n %s: %s", k, v)
}
}
return &Response{
StatusCode: appErr.Code,
Body: msg,
}
}
return &Response{
StatusCode: 500,
Body: fmt.Sprintf("不明なエラー: %v", err),
}
}
// ==================== メイン関数 ====================
func main() {
// Recoveryミドルウェアでハンドラをラップ
safeHandler := RecoveryMiddleware(handleRequest)
// さまざまなシナリオをテスト
testCases := []struct {
name string
req *Request
}{
{
name: "正常なリクエスト",
req: &Request{UserID: 1, Path: "/users", IsAdmin: true},
},
{
name: "無効なパラメータ",
req: &Request{UserID: -1, Path: "/users"},
},
{
name: "未認証",
req: &Request{UserID: 0, Path: "/users"},
},
{
name: "権限不足",
req: &Request{UserID: 2, Path: "/admin", IsAdmin: false},
},
{
name: "リソースが見つからない",
req: &Request{UserID: 1, Path: "/unknown"},
},
{
name: "panic の発生(自動回復)",
req: &Request{UserID: 1, Path: "/crash"},
},
}
for _, tc := range testCases {
fmt.Printf("=== %s ===\n", tc.name)
resp, err := safeHandler(tc.req)
if err != nil {
resp = formatError(err)
}
fmt.Printf(" ステータス: %d\n", resp.StatusCode)
fmt.Printf(" レスポンス: %s\n\n", resp.Body)
}
}
出力:
=== 正常なリクエスト ===
ステータス: 200
レスポンス: ユーザーリスト
=== 無効なパラメータ ===
ステータス: 400
レスポンス: エラー [400]: 不正なリクエスト
詳細: user_id は正の整数でなければなりません
コンテキスト:
user_id: -1
=== 未認証 ===
ステータス: 401
レスポンス: エラー [401]: 認証されていません
詳細: 認証資格情報がありません
=== 権限不足 ===
ステータス: 403
レスポンス: エラー [403]: アクセスが禁止されています
詳細: 管理者権限が必要です
コンテキスト:
user_id: 2
=== リソースが見つからない ===
ステータス: 404
レスポンス: エラー [404]: リソースが見つかりません
詳細: パス /unknown のリソースが見つかりません
=== panic の発生(自動回復) ===
[PANIC] runtime error: invalid memory address or nil pointer dereference
スタック:
...
ステータス: 500
レスポンス: エラー [500]: 内部サーバーエラー
詳細: panic: runtime error: invalid memory address or nil pointer dereference
現実世界の使用場面
場面1:データベーストランザクションのロールバック
複数のステップを含むデータベース操作では、いずれかの失敗時にすでに実行された操作をロールバックする必要があります。
package main
import (
"errors"
"fmt"
)
var (
ErrBalanceInsufficient = errors.New("残高不足です")
ErrAccountFrozen = errors.New("アカウントが凍結されています")
)
// TxError はトランザクションエラーで、失敗したステップを記録
type TxError struct {
Step string
Cause error
Actions []string // ロールバックが必要な実行済みアクション
}
func (e *TxError) Error() string {
return fmt.Sprintf("トランザクション失敗 [ステップ: %s]: %v", e.Step, e.Cause)
}
func (e *TxError) Unwrap() error {
return e.Cause
}
// TransferService は振込サービス
type TransferService struct {
balances map[string]float64
frozen map[string]bool
}
// Transfer は振込トランザクションを実行する
func (s *TransferService) Transfer(from, to string, amount float64) error {
var executedSteps []string
// ステップ1: 送金元アカウントの検証
if s.frozen[from] {
return &TxError{
Step: "送金元アカウントの検証",
Cause: ErrAccountFrozen,
}
}
executedSteps = append(executedSteps, "送金元アカウントのロック")
// ステップ2: 残高の確認
if s.balances[from] < amount {
return &TxError{
Step: "残高の確認",
Cause: ErrBalanceInsufficient,
Actions: executedSteps,
}
}
executedSteps = append(executedSteps, "残高チェック通過")
// ステップ3: 送金元から引落し
s.balances[from] -= amount
executedSteps = append(executedSteps, fmt.Sprintf("%.2fを引落し", amount))
// ステップ4: 送金先アカウントの検証
if s.frozen[to] {
return &TxError{
Step: "送金先アカウントの検証",
Cause: ErrAccountFrozen,
Actions: executedSteps, // 引落し済みの金額をロールバックする必要あり
}
}
// ステップ5: 送金先に入金
s.balances[to] += amount
fmt.Printf(" 振込成功: %s -> %s、金額: %.2f\n", from, to, amount)
return nil
}
func main() {
svc := &TransferService{
balances: map[string]float64{
"Alice": 1000,
"Bob": 500,
"Carol": 0,
},
frozen: map[string]bool{
"Carol": true,
},
}
tests := []struct {
name string
from string
to string
amt float64
}{
{"正常な振込", "Alice", "Bob", 200},
{"残高不足", "Alice", "Bob", 9999},
{"送金先アカウント凍結", "Alice", "Carol", 100},
}
for _, t := range tests {
fmt.Printf("--- %s ---\n", t.name)
fmt.Printf(" 事前: %s=%.2f、%s=%.2f\n",
t.from, svc.balances[t.from], t.to, svc.balances[t.to])
err := svc.Transfer(t.from, t.to, t.amt)
if err != nil {
fmt.Printf(" 失敗: %v\n", err)
// ロールバックが必要かチェック
var txErr *TxError
if errors.As(err, &txErr) && len(txErr.Actions) > 0 {
fmt.Printf(" %d件の実行済みアクションをロールバック:\n", len(txErr.Actions))
for _, action := range txErr.Actions {
fmt.Printf(" - 取消: %s\n", action)
}
// 実際のプロジェクトではここでロールバックロジックを実行
// 例: svc.balances[from] += amount
}
}
fmt.Printf(" 事後: %s=%.2f、%s=%.2f\n\n",
t.from, svc.balances[t.from], t.to, svc.balances[t.to])
}
}
場面2:バッチファイル処理とエラー収集
複数のファイルを処理する際、最初の失敗で停止するのではなく、すべてのエラーを収集してまとめて報告します。
package main
import (
"errors"
"fmt"
"strings"
)
var (
ErrFileCorrupted = errors.New("ファイルが破損しています")
ErrPermission = errors.New("権限が不足しています")
ErrDiskFull = errors.New("ディスク容量が不足しています")
)
// FileError は単一ファイルのエラー
type FileError struct {
Path string
Op string
Cause error
}
func (e *FileError) Error() string {
return fmt.Sprintf("[%s] %s: %v", e.Op, e.Path, e.Cause)
}
func (e *FileError) Unwrap() error {
return e.Cause
}
// BatchResult はバッチ処理結果
type BatchResult struct {
Success []string
Failed []FileError
}
func (r *BatchResult) HasErrors() bool {
return len(r.Failed) > 0
}
func (r *BatchResult) Summary() string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("処理完了: %d件成功、%d件失敗\n",
len(r.Success), len(r.Failed)))
if len(r.Success) > 0 {
sb.WriteString("成功したファイル:\n")
for _, f := range r.Success {
sb.WriteString(fmt.Sprintf(" ✓ %s\n", f))
}
}
if len(r.Failed) > 0 {
sb.WriteString("失敗したファイル:\n")
for _, e := range r.Failed {
sb.WriteString(fmt.Sprintf(" ✗ %s (%s: %v)\n", e.Path, e.Op, e.Cause))
}
}
return sb.String()
}
// processFile は単一ファイルの処理をシミュレート
func processFile(path string) error {
// さまざまなエラーをシミュレート
fileErrors := map[string]error{
"corrupted.txt": ErrFileCorrupted,
"secret.txt": ErrPermission,
"huge.txt": ErrDiskFull,
}
if err, exists := fileErrors[path]; exists {
return &FileError{
Path: path,
Op: "process",
Cause: err,
}
}
return nil
}
// processFiles はファイルをバッチ処理する
func processFiles(paths []string) *BatchResult {
result := &BatchResult{}
for _, path := range paths {
err := processFile(path)
if err != nil {
result.Failed = append(result.Failed, FileError{
Path: path,
Op: "process",
Cause: errors.Unwrap(err),
})
} else {
result.Success = append(result.Success, path)
}
}
return result
}
func main() {
files := []string{
"report.pdf",
"data.csv",
"corrupted.txt",
"image.png",
"secret.txt",
"huge.txt",
"readme.md",
}
fmt.Printf("%d件のファイルを処理開始...\n\n", len(files))
result := processFiles(files)
fmt.Println(result.Summary())
// エラータイプに基づいて異なる提案を提供
for _, fe := range result.Failed {
switch {
case errors.Is(fe.Cause, ErrFileCorrupted):
fmt.Printf("提案: %s はバックアップから復元が必要です\n", fe.Path)
case errors.Is(fe.Cause, ErrPermission):
fmt.Printf("提案: %s のファイル権限を確認してください\n", fe.Path)
case errors.Is(fe.Cause, ErrDiskFull):
fmt.Printf("提案: ディスク容量を空けて %s を再試行してください\n", fe.Path)
}
}
}
❓ よくある質問
質問1:panic と error の戻し、いつ使うべき?
原則: 予想される失敗には error を使用し、「決して起きるべきでない」プログラミングエラーには panic を使用します。
// ✓ 正しい: error を返す — ユーザー入力は予測不可能
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数はゼロにできません")
}
return a / b, nil
}
// ✓ 正しい: panic を使用 — これはプログラマーのバグで、nilを渡すべきでない
func MustNotNil(v interface{}) {
if v == nil {
panic("値はnilであってはなりません。これは呼び出し側のプログラミングエラーです")
}
}
実践的なルール:
- error を返す: ファイル操作、ネットワークリクエスト、ユーザー入力検証、ビジネスロジックの失敗
- panic を使用:
init()初期化エラー、回復不可能なプログラムエラー、テストのt.Fatal - 決して使わない: 通常のビジネスエラーに
panicを使用しない
質問2:errors.Is と errors.As の違いは?
// errors.Is: エラーチェーンに特定のセンチネルエラーが含まれているかチェック
// 次と同等: err == target || err.Unwrap() == target || ...
errors.Is(err, ErrNotFound) // true/false
// errors.As: エラーチェーンから特定のエラータイプを抽出
// 型アサーションに似ているが、エラーチェーン全体を走査
var appErr *AppError
errors.As(err, &appErr) // true + 代入、または false
// 例え話:
// errors.Is → 「あなたはZhang Sanですか?」(アイデンティティの確認)
// errors.As → 「あなたはプログラマーですか?如果是的话、スキルを教えてください」(情報の抽出)
質問3:なぜエラーメッセージは小文字で句読点なしが推奨されるの?
Go公式およびコミュニティの慣例:
// ✓ 推奨
fmt.Errorf("ファイル %s の読み込みに失敗しました: %w", name, err)
// 出力: ファイル config.yaml の読み込みに失敗しました: 権限がありません
// ✗ 非推奨
fmt.Errorf("ファイル %s の読み込みに失敗しました.: %w", name, err) // 不要な句読点
fmt.Errorf("ファイル %s の読み込みに失敗しました!: %w", name, err) // 不要な感嘆符
fmt.Errorf("Read file %s failed: %w", name, err) // 言語が一貫しない
理由は、エラーはしばしばエラーチェーンに連結され、小文字で句読点なしの方が読みやすいからです:
データベースの初期化: postgresへの接続: dial tcp 127.0.0.1:5432: 接続が拒否されました
質問4:ラップされたエラーはどうやって比較するの?
ラップされたエラーを直接 == で比較しても動作しません。errors.Is を使用する必要があります:
var ErrSentinel = errors.New("sentinel")
wrapped := fmt.Errorf("コンテキスト: %w", ErrSentinel)
// ✗ 誤り: 直接比較は失敗
fmt.Println(wrapped == ErrSentinel) // false
// ✓ 正しい: errors.Is を使用
fmt.Println(errors.Is(wrapped, ErrSentinel)) // true
// errors.Is はエラーチェーン全体を走査:
// wrapped → Unwrap() → ErrSentinel → 一致!
📖 まとめ
| トピック | 重要ポイント |
|---|---|
error インターフェース |
Error() string メソッドを実装するだけ |
| エラーの作成 | errors.New で簡単なエラー、fmt.Errorf + %w でラップエラー |
| エラーのチェック | errors.Is で型チェック、errors.As で型抽出 |
| カスタムエラー | Error() + Unwrap() メソッドを実装し、ビジネスコンテキストを含める |
panic/recover |
回復不可能なエラーのみ。defer で recover してキャッチ |
| エラー哲学 | 明示的な処理、各エラーをチェック、エラーは値であり例外ではない |
| エラーラッピング | %w で基礎エラーを保持、%v で破棄 |
| コミュニティ慣例 | エラーメッセージは小文字で句読点なし、最後の戻り値として返す |
Goエラー処理の3つの黄金ルール:
- すぐにチェック —
errorを受け取ったら、すぐにerr != nilをチェック - 各層でラップ — 各層でコンテキストを追加し、
%wで元のエラーを保持 - 优雅に降格 — 可能なら回復、できなければ上位に報告、最後の手段としてのみpanic
📝 演習
演習1:リトライ付きファイルリーダーの実装
以下の要件で ReadWithRetry(path string, maxRetries int) ([]byte, error) 関数を書いてください:
- 最大
maxRetries回リトライ - 各失敗後にリトライ回数を表示
ErrMaxRetriesExceededセンチネルエラーを定義し、リトライ回数を超えた場合に返すfmt.Errorfを使って基礎エラーをラップしつつコンテキストを保持
// 参考フレームワーク
var ErrMaxRetriesExceeded = errors.New("最大リトライ回数を超えました")
func ReadWithRetry(path string, maxRetries int) ([]byte, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
data, err := os.ReadFile(path)
if err == nil {
return data, nil
}
lastErr = err
fmt.Printf(" リトライ%d回目失敗: %v\n", i+1, err)
}
return nil, fmt.Errorf("%s の読み込み: %w (最後のエラー: %v)", path, ErrMaxRetriesExceeded, lastErr)
}
演習2:エラーレベルシステムの構築
以下のエラーレベルをサポートするエラーレベルシステムを実装してください:
type Level int
const (
LevelDebug Level = iota
LevelInfo
LevelWarn
LevelError
LevelFatal
)
type LeveledError struct {
Level Level
Message string
Cause error
}
要件:
errorインターフェースを実装Unwrap() errorメソッドを実装IsLevel(err error, level Level) bool関数を記述- さまざまなエラーのレベルチェックをテスト
演習3:エラーコレクターの実装(並行安全)
並行シナリオで複数のgoroutineからエラーを収集する ErrorCollector 型を書いてください:
type ErrorCollector struct {
// 必要なフィールド
}
// Add はエラーを追加する(並行安全)
func (c *ErrorCollector) Add(err error) { ... }
// Errors は収集したすべてのエラーを返す
func (c *ErrorCollector) Errors() []error { ... }
// HasErrors はエラーがあるかチェック
func (c *ErrorCollector) HasErrors() bool { ... }
// Error はすべてのエラーを1つのエラーにまとめる
func (c *ErrorCollector) Error() error { ... }
要件:
sync.Mutexまたはsync.RWMutexを使って並行安全性を確保- テストを記述:複数のgoroutineを起動して
Addを並行呼び出し、収集結果を検証
次のレッスン
このレッスンを完了したら、レッスン11:パッケージとモジュールに進んで、Goのコードの整理、依存関係の管理、独自パッケージの公開方法を学びましょう。



