課題11: エラー処理の実装
課題概要
この課題では、実用的なファイル処理システムを構築し、Goのエラー処理機能を包括的に活用します。センチネルエラー、カスタムエラー型、エラーラッピング、そしてerrors.IsとErrors.Asを使った高度なエラーハンドリングを実装します。
マンダトリー要件
要件1: カスタムエラー型の実装(20点)
ファイル操作に関するカスタムエラー型を実装してください。
ファイル: errors.go
package fileops
import "fmt"
// FileError はファイル操作のエラーを表す
type FileError struct {
Op string // 操作名("read", "write", "delete"など)
Path string // ファイルパス
Err error // 元のエラー
}
func (e *FileError) Error() string {
return fmt.Sprintf("ファイル操作エラー [%s] %s: %v", e.Op, e.Path, e.Err)
}
func (e *FileError) Unwrap() error {
return e.Err
}
// ValidationError は入力検証のエラーを表す
type ValidationError struct {
Field string
Value string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("検証エラー [%s]: %s (値: %s)", e.Field, e.Message, e.Value)
}
実装すべき内容:
FileError: ファイル操作のエラー(操作種別、パス、元エラーを保持)ValidationError: 入力検証のエラー(フィールド名、値、メッセージを保持)- 両方とも
Error()メソッドを実装 FileErrorはUnwrap()メソッドも実装
要件2: センチネルエラーの定義(15点)
よくあるエラー状態をセンチネルエラーとして定義してください。
ファイル: errors.go(続き)
import "errors"
// センチネルエラー
var (
ErrFileNotFound = errors.New("ファイルが見つかりません")
ErrPermissionDenied = errors.New("権限が拒否されました")
ErrInvalidPath = errors.New("無効なパスです")
ErrFileExists = errors.New("ファイルは既に存在します")
ErrEmptyFile = errors.New("ファイルが空です")
)
実装すべき内容:
- 5つ以上のセンチネルエラーを定義
- ファイル操作で発生しうる一般的なエラー状態を網羅
要件3: ファイル処理システムの実装(25点)
エラー処理を含む包括的なファイル操作システムを実装してください。
ファイル: fileops.go
package fileops
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// FileManager はファイル操作を管理する
type FileManager struct {
baseDir string
}
func NewFileManager(baseDir string) (*FileManager, error) {
// baseDirの検証
if baseDir == "" {
return nil, &ValidationError{
Field: "baseDir",
Value: baseDir,
Message: "ベースディレクトリが空です",
}
}
// ディレクトリの存在確認
info, err := os.Stat(baseDir)
if err != nil {
if os.IsNotExist(err) {
return nil, &FileError{
Op: "stat",
Path: baseDir,
Err: ErrFileNotFound,
}
}
return nil, &FileError{
Op: "stat",
Path: baseDir,
Err: fmt.Errorf("ディレクトリ情報取得に失敗: %w", err),
}
}
if !info.IsDir() {
return nil, &ValidationError{
Field: "baseDir",
Value: baseDir,
Message: "ディレクトリではありません",
}
}
return &FileManager{baseDir: baseDir}, nil
}
// ReadFile はファイルを読み込む
func (fm *FileManager) ReadFile(filename string) (string, error) {
// ファイル名の検証
if err := fm.validateFilename(filename); err != nil {
return "", err
}
// フルパスの構築
fullPath := filepath.Join(fm.baseDir, filename)
// ファイル読み込み
data, err := os.ReadFile(fullPath)
if err != nil {
if os.IsNotExist(err) {
return "", &FileError{
Op: "read",
Path: filename,
Err: ErrFileNotFound,
}
}
if os.IsPermission(err) {
return "", &FileError{
Op: "read",
Path: filename,
Err: ErrPermissionDenied,
}
}
return "", &FileError{
Op: "read",
Path: filename,
Err: fmt.Errorf("ファイル読み込みに失敗: %w", err),
}
}
// 空ファイルチェック
if len(data) == 0 {
return "", &FileError{
Op: "read",
Path: filename,
Err: ErrEmptyFile,
}
}
return string(data), nil
}
// WriteFile はファイルに書き込む
func (fm *FileManager) WriteFile(filename, content string) error {
// ファイル名の検証
if err := fm.validateFilename(filename); err != nil {
return err
}
// コンテンツの検証
if content == "" {
return &ValidationError{
Field: "content",
Value: content,
Message: "コンテンツが空です",
}
}
// フルパスの構築
fullPath := filepath.Join(fm.baseDir, filename)
// ファイルの存在確認
if _, err := os.Stat(fullPath); err == nil {
return &FileError{
Op: "write",
Path: filename,
Err: ErrFileExists,
}
}
// ファイル書き込み
if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
if os.IsPermission(err) {
return &FileError{
Op: "write",
Path: filename,
Err: ErrPermissionDenied,
}
}
return &FileError{
Op: "write",
Path: filename,
Err: fmt.Errorf("ファイル書き込みに失敗: %w", err),
}
}
return nil
}
// DeleteFile はファイルを削除する
func (fm *FileManager) DeleteFile(filename string) error {
// ファイル名の検証
if err := fm.validateFilename(filename); err != nil {
return err
}
// フルパスの構築
fullPath := filepath.Join(fm.baseDir, filename)
// ファイル削除
if err := os.Remove(fullPath); err != nil {
if os.IsNotExist(err) {
return &FileError{
Op: "delete",
Path: filename,
Err: ErrFileNotFound,
}
}
if os.IsPermission(err) {
return &FileError{
Op: "delete",
Path: filename,
Err: ErrPermissionDenied,
}
}
return &FileError{
Op: "delete",
Path: filename,
Err: fmt.Errorf("ファイル削除に失敗: %w", err),
}
}
return nil
}
// validateFilename はファイル名を検証する
func (fm *FileManager) validateFilename(filename string) error {
if filename == "" {
return &ValidationError{
Field: "filename",
Value: filename,
Message: "ファイル名が空です",
}
}
// パストラバーサル攻撃の防止
if strings.Contains(filename, "..") {
return &FileError{
Op: "validate",
Path: filename,
Err: ErrInvalidPath,
}
}
// 絶対パスの禁止
if filepath.IsAbs(filename) {
return &FileError{
Op: "validate",
Path: filename,
Err: ErrInvalidPath,
}
}
return nil
}
実装すべき内容:
NewFileManager: ベースディレクトリを指定してマネージャーを作成ReadFile: ファイル読み込み(エラー処理含む)WriteFile: ファイル書き込み(エラー処理含む)DeleteFile: ファイル削除(エラー処理含む)validateFilename: ファイル名の検証(セキュリティチェック含む)
要件4: エラー処理とハンドリング(20点)
エラーを適切に処理し、詳細な情報を提供するメインプログラムを実装してください。
ファイル: main.go
package main
import (
"errors"
"fmt"
"os"
"yourmodule/fileops"
)
func main() {
// 作業ディレクトリの作成
workDir := "./testdata"
if err := os.MkdirAll(workDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "作業ディレクトリ作成に失敗: %v\n", err)
os.Exit(1)
}
// FileManagerの作成
fm, err := fileops.NewFileManager(workDir)
if err != nil {
handleError(err)
os.Exit(1)
}
// テストファイルの書き込み
fmt.Println("=== ファイル書き込みテスト ===")
if err := fm.WriteFile("test.txt", "Hello, Go!"); err != nil {
handleError(err)
} else {
fmt.Println("✓ ファイル書き込み成功")
}
// ファイル読み込み
fmt.Println("\n=== ファイル読み込みテスト ===")
content, err := fm.ReadFile("test.txt")
if err != nil {
handleError(err)
} else {
fmt.Printf("✓ ファイル読み込み成功: %s\n", content)
}
// 存在しないファイルの読み込み
fmt.Println("\n=== エラーケース: 存在しないファイル ===")
_, err = fm.ReadFile("notfound.txt")
if err != nil {
handleError(err)
}
// 無効なファイル名
fmt.Println("\n=== エラーケース: 無効なファイル名 ===")
err = fm.WriteFile("../../../etc/passwd", "malicious")
if err != nil {
handleError(err)
}
// 空のコンテンツ
fmt.Println("\n=== エラーケース: 空のコンテンツ ===")
err = fm.WriteFile("empty.txt", "")
if err != nil {
handleError(err)
}
// ファイル削除
fmt.Println("\n=== ファイル削除テスト ===")
if err := fm.DeleteFile("test.txt"); err != nil {
handleError(err)
} else {
fmt.Println("✓ ファイル削除成功")
}
// クリーンアップ
os.RemoveAll(workDir)
}
// handleError はエラーを詳細に処理する
func handleError(err error) {
fmt.Println("エラーが発生しました:")
// FileErrorの処理
var fileErr *fileops.FileError
if errors.As(err, &fileErr) {
fmt.Printf(" 種類: ファイルエラー\n")
fmt.Printf(" 操作: %s\n", fileErr.Op)
fmt.Printf(" パス: %s\n", fileErr.Path)
// センチネルエラーのチェック
if errors.Is(err, fileops.ErrFileNotFound) {
fmt.Println(" 原因: ファイルが見つかりません")
} else if errors.Is(err, fileops.ErrPermissionDenied) {
fmt.Println(" 原因: 権限が不足しています")
} else if errors.Is(err, fileops.ErrInvalidPath) {
fmt.Println(" 原因: パスが無効です")
} else if errors.Is(err, fileops.ErrFileExists) {
fmt.Println(" 原因: ファイルは既に存在します")
} else if errors.Is(err, fileops.ErrEmptyFile) {
fmt.Println(" 原因: ファイルが空です")
}
}
// ValidationErrorの処理
var valErr *fileops.ValidationError
if errors.As(err, &valErr) {
fmt.Printf(" 種類: 検証エラー\n")
fmt.Printf(" フィールド: %s\n", valErr.Field)
fmt.Printf(" 値: %s\n", valErr.Value)
fmt.Printf(" メッセージ: %s\n", valErr.Message)
}
fmt.Printf(" 詳細: %v\n", err)
}
実装すべき内容:
- 各種ファイル操作のテスト
- エラーケースの網羅的なテスト
handleError関数による詳細なエラー処理errors.Asを使った型アサーションerrors.Isを使ったセンチネルエラーの判定
期待される出力
=== ファイル書き込みテスト ===
✓ ファイル書き込み成功
=== ファイル読み込みテスト ===
✓ ファイル読み込み成功: Hello, Go!
=== エラーケース: 存在しないファイル ===
エラーが発生しました:
種類: ファイルエラー
操作: read
パス: notfound.txt
原因: ファイルが見つかりません
詳細: ファイル操作エラー [read] notfound.txt: ファイルが見つかりません
=== エラーケース: 無効なファイル名 ===
エラーが発生しました:
種類: ファイルエラー
操作: validate
パス: ../../../etc/passwd
原因: パスが無効です
詳細: ファイル操作エラー [validate] ../../../etc/passwd: 無効なパスです
=== エラーケース: 空のコンテンツ ===
エラーが発生しました:
種類: 検証エラー
フィールド: content
値:
メッセージ: コンテンツが空です
詳細: 検証エラー [content]: コンテンツが空です (値: )
=== ファイル削除テスト ===
✓ ファイル削除成功
ボーナス課題
> ボーナス: これらはオプションです。マンダトリー部分が完了してから取り組んでください。
ボーナス1: エラーのログ機能(10点)
エラーを構造化されたログとして記録する機能を追加してください。
ファイル: errorlog.go
package fileops
import (
"encoding/json"
"fmt"
"os"
"time"
)
// ErrorLog はエラーログのエントリ
type ErrorLog struct {
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"`
Message string `json:"message"`
Details map[string]string `json:"details"`
}
// ErrorLogger はエラーをログに記録する
type ErrorLogger struct {
logFile *os.File
}
func NewErrorLogger(filename string) (*ErrorLogger, error) {
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, fmt.Errorf("ログファイルを開けません: %w", err)
}
return &ErrorLogger{logFile: file}, nil
}
func (l *ErrorLogger) Log(err error) error {
entry := ErrorLog{
Timestamp: time.Now(),
Message: err.Error(),
Details: make(map[string]string),
}
// エラー型に応じて詳細を追加
var fileErr *FileError
if errors.As(err, &fileErr) {
entry.Type = "FileError"
entry.Details["operation"] = fileErr.Op
entry.Details["path"] = fileErr.Path
}
var valErr *ValidationError
if errors.As(err, &valErr) {
entry.Type = "ValidationError"
entry.Details["field"] = valErr.Field
entry.Details["value"] = valErr.Value
}
// JSON形式で書き込み
data, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("ログのシリアライズに失敗: %w", err)
}
if _, err := l.logFile.Write(append(data, '\n')); err != nil {
return fmt.Errorf("ログの書き込みに失敗: %w", err)
}
return nil
}
func (l *ErrorLogger) Close() error {
return l.logFile.Close()
}
ボーナス2: リトライ機構(5点)
一時的なエラーに対して自動的にリトライする機能を実装してください。
package fileops
import (
"errors"
"time"
)
// RetryConfig はリトライの設定
type RetryConfig struct {
MaxAttempts int
Delay time.Duration
}
// WithRetry は関数をリトライ機能付きで実行する
func WithRetry(config RetryConfig, fn func() error) error {
var lastErr error
for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
err := fn()
if err == nil {
return nil
}
lastErr = err
// リトライ不可能なエラーはすぐに返す
if !isRetryable(err) {
return err
}
if attempt < config.MaxAttempts {
time.Sleep(config.Delay)
}
}
return fmt.Errorf("リトライ上限に達しました (%d回): %w", config.MaxAttempts, lastErr)
}
// isRetryable はエラーがリトライ可能かを判定
func isRetryable(err error) bool {
// 権限エラーやファイル未検出はリトライ不可
if errors.Is(err, ErrPermissionDenied) || errors.Is(err, ErrFileNotFound) {
return false
}
// ValidationErrorはリトライ不可
var valErr *ValidationError
if errors.As(err, &valErr) {
return false
}
return true
}
ボーナス3: エラー統計レポート(5点)
発生したエラーの統計情報を収集し、レポートを生成してください。
package fileops
import (
"fmt"
"sync"
)
// ErrorStats はエラー統計を管理する
type ErrorStats struct {
mu sync.Mutex
counts map[string]int
samples map[string]error
}
func NewErrorStats() *ErrorStats {
return &ErrorStats{
counts: make(map[string]int),
samples: make(map[string]error),
}
}
func (s *ErrorStats) Record(err error) {
s.mu.Lock()
defer s.mu.Unlock()
var errType string
var fileErr *FileError
if errors.As(err, &fileErr) {
errType = fmt.Sprintf("FileError:%s", fileErr.Op)
} else {
var valErr *ValidationError
if errors.As(err, &valErr) {
errType = fmt.Sprintf("ValidationError:%s", valErr.Field)
} else {
errType = "Other"
}
}
s.counts[errType]++
if _, exists := s.samples[errType]; !exists {
s.samples[errType] = err
}
}
func (s *ErrorStats) Report() string {
s.mu.Lock()
defer s.mu.Unlock()
report := "=== エラー統計レポート ===\n"
total := 0
for errType, count := range s.counts {
report += fmt.Sprintf("%s: %d回\n", errType, count)
report += fmt.Sprintf(" サンプル: %v\n", s.samples[errType])
total += count
}
report += fmt.Sprintf("\n合計: %d回\n", total)
return report
}
評価基準
| 項目 | 配点 | 詳細 |
|---|---|---|
| カスタムエラー型 | 20点 | FileErrorとValidationErrorが正しく実装されている |
| センチネルエラー | 15点 | 適切なセンチネルエラーが定義されている |
| ファイル処理システム | 25点 | 全てのファイル操作が正しく動作し、エラー処理が適切 |
| エラーハンドリング | 20点 | errors.IsとErrors.Asを使った詳細な処理ができている |
| **ボーナス1** | 10点 | 構造化ログが正しく実装されている |
| **ボーナス2** | 5点 | リトライ機構が適切に動作する |
| **ボーナス3** | 5点 | 統計レポートが有用な情報を提供している |
提出方法
以下のファイルを提出してください:
submission/
├── go.mod
├── errors.go # カスタムエラー型とセンチネルエラー
├── fileops.go # ファイル操作システム
├── main.go # メインプログラム
└── bonus/ # ボーナス課題(オプション)
├── errorlog.go # エラーログ機能
├── retry.go # リトライ機構
└── stats.go # エラー統計
ヒント
fmt.Errorf("...: %w", err)で元のエラーを保持filepath.Clean()やfilepath.Abs()も活用sync.Mutexでデータ保護