package main import ( "context" "errors" "fmt" "strings" "testing" ) // ------------------------------ // 错误类型定义(核心优化) // ------------------------------ // FileError 自定义文件服务错误结构体 type FileError struct { Code int // 错误码 Module string // 所属模块 Message string // 错误信息 Err error // 原始错误(支持错误包装/解包) } // Error 实现 error 接口 func (e *FileError) Error() string { if e.Err != nil { return fmt.Sprintf("[%s:%d] %s: %v", e.Module, e.Code, e.Message, e.Err) } return fmt.Sprintf("[%s:%d] %s", e.Module, e.Code, e.Message) } // Unwrap 实现 errors.Unwrap 支持错误链解包 func (e *FileError) Unwrap() error { return e.Err } // 预定义错误(使用 sentinel error 规范) var ErrFileNotFound = errors.New("file not found") // ------------------------------ // 业务服务实现 // ------------------------------ // FileService 基础文件服务 func FileService(ctx context.Context, filename string) error { // 模块名称常量 const module = "FileService" // 1. 空文件名校验(400) if filename == "" { return &FileError{ Code: 400, Module: module, Message: "filename cannot be empty", } } // 2. 文件不存在模拟(404) return &FileError{ Code: 404, Module: module, Message: "file does not exist", Err: ErrFileNotFound, // 包装原始哨兵错误 } } // AdvancedFileService 高级文件服务(包装基础服务) func AdvancedFileService(ctx context.Context, filename string) error { // 调用底层服务并包装错误信息 if err := FileService(ctx, filename); err != nil { return fmt.Errorf("advanced file service failed: %s %w", filename, err) } return nil } // ------------------------------go vet // 测试用例 // ------------------------------ func TestFileService(t *testing.T) { ctx := context.Background() // 子测试用例(清晰分组,便于定位失败) t.Run("file not found returns 404 error", func(t *testing.T) { err := FileService(ctx, "test.txt") // 断言哨兵错误 if !errors.Is(err, ErrFileNotFound) { t.Fatalf("期望错误: %v, 实际错误: %v", ErrFileNotFound, err) } // 断言自定义错误类型与字段 var fErr *FileError if !errors.As(err, &fErr) { t.Fatal("期望返回 *FileError 类型") } if fErr.Code != 404 { t.Errorf("错误码期望 404, 实际 %d", fErr.Code) } if fErr.Module != "FileService" { t.Errorf("模块期望 FileService, 实际 %s", fErr.Module) } }) t.Run("empty filename returns 400 error", func(t *testing.T) { err := FileService(ctx, "") if err == nil { t.Fatal("空文件名必须返回错误") } var fErr *FileError if !errors.As(err, &fErr) { t.Fatal("期望返回 *FileError 类型") } if fErr.Code != 400 { t.Errorf("错误码期望 400, 实际 %d", fErr.Code) } }) } func TestErrorWrapping(t *testing.T) { original := errors.New("original error") wrapped := fmt.Errorf("wrapped: %w", original) // 测试 errors.Is 错误链匹配 if !errors.Is(wrapped, original) { t.Error("errors.Is 应当匹配包装后的错误") } // 测试 errors.Unwrap 解包 unwrapped := errors.Unwrap(wrapped) if unwrapped != original { t.Errorf("解包后应当返回原始错误, 实际: %v", unwrapped) } } func TestAdvancedFileService(t *testing.T) { ctx := context.Background() err := AdvancedFileService(ctx, "nonexistent.txt") if err == nil { t.Fatal("必须返回错误") } // 错误信息包含文件名 if !strings.Contains(err.Error(), "nonexistent.txt") { t.Errorf("错误信息应当包含文件名, 实际: %v", err) } // 错误链中能找到自定义 FileError var fErr *FileError if !errors.As(err, &fErr) { t.Error("错误链中应当包含 *FileError") } }