12 - 错误处理:error 接口、errors 包、自定义错误、panic/recover
12 - 错误处理
12.1 error 接口
// error 是一个内置接口
type error interface {
Error() string
}
package main
import (
"errors"
"fmt"
"os"
)
func main() {
// 错误处理的基本模式
f, err := os.Open("nonexistent.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer f.Close()
}
12.2 创建错误
package main
import (
"errors"
"fmt"
)
func main() {
// errors.New
err1 := errors.New("something went wrong")
fmt.Println(err1)
// fmt.Errorf(格式化错误信息)
name := "Alice"
err2 := fmt.Errorf("user %s not found", name)
fmt.Println(err2)
// Go 1.13+ %w 包装错误
baseErr := errors.New("connection refused")
wrappedErr := fmt.Errorf("failed to connect to database: %w", baseErr)
fmt.Println(wrappedErr)
// 多个错误包装
err3 := fmt.Errorf("outer: %w", fmt.Errorf("middle: %w", errors.New("inner")))
fmt.Println(err3)
}
12.3 检查错误
package main
import (
"errors"
"fmt"
"os"
)
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
func findUser(id int) (string, error) {
if id < 0 {
return "", ErrNotFound
}
if id == 0 {
return "", ErrPermission
}
return "Alice", nil
}
func main() {
// 比较错误
_, err := findUser(-1)
if err == ErrNotFound {
fmt.Println("用户不存在")
}
// errors.Is(推荐,支持包装的错误)
wrappedErr := fmt.Errorf("query failed: %w", ErrNotFound)
if errors.Is(wrappedErr, ErrNotFound) {
fmt.Println("用户不存在(通过 Is 检查)")
}
// errors.As(提取特定类型的错误)
_, err = os.Open("nonexistent.txt")
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("路径错误: %s - %s\n", pathErr.Path, pathErr.Err)
}
// 检查多个错误
_, err = findUser(0)
switch {
case errors.Is(err, ErrNotFound):
fmt.Println("未找到")
case errors.Is(err, ErrPermission):
fmt.Println("权限不足")
default:
fmt.Println("其他错误:", err)
}
}
errors.Is vs errors.As
| 函数 | 用途 | 行为 |
|---|
errors.Is(err, target) | 检查错误链中是否有特定错误值 | 比较值(支持 Unwrap) |
errors.As(err, &target) | 从错误链中提取特定类型 | 类型断言(支持 Unwrap) |
12.4 自定义错误类型
package main
import (
"fmt"
"time"
)
// 自定义错误类型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: field %s - %s", e.Field, e.Message)
}
// 带上下文的错误
type AppError struct {
Code int
Message string
Err error
CreatedAt time.Time
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 构造函数
func NewValidationError(field, message string) *ValidationError {
return &ValidationError{Field: field, Message: message}
}
func NewAppError(code int, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
CreatedAt: time.Now(),
}
}
func validateAge(age int) error {
if age < 0 {
return NewValidationError("age", "不能为负数")
}
if age > 150 {
return NewValidationError("age", "不能超过150")
}
return nil
}
func getUser(id int) (string, error) {
if id < 0 {
return "", NewAppError(404, "user not found", ErrNotFound)
}
return "Alice", nil
}
func main() {
err := validateAge(-1)
if err != nil {
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("字段: %s, 原因: %s\n", ve.Field, ve.Message)
}
}
}
12.5 错误处理模式
经典模式
func doSomething() error {
result, err := step1()
if err != nil {
return fmt.Errorf("step1 failed: %w", err)
}
result2, err := step2(result)
if err != nil {
return fmt.Errorf("step2 failed: %w", err)
}
return step3(result2)
}
哨兵错误
var (
ErrUserNotFound = errors.New("user not found")
ErrUserExists = errors.New("user already exists")
ErrInvalidEmail = errors.New("invalid email")
ErrDatabaseTimeout = errors.New("database timeout")
)
错误包装链
ErrDatabaseTimeout
→ "failed to query user: database timeout"
→ "API handler error: failed to query user: database timeout"
12.6 panic 和 recover
package main
import "fmt"
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil
}
func main() {
// panic 会终止程序
// divide(10, 0) // panic: division by zero
// recover 捕获 panic
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result)
}
}
panic 使用原则
✅ 使用 panic 的场景:
- 程序初始化时无法继续(如配置加载失败)
- 不可能发生的情况(逻辑上不可达的代码)
- 内部不可恢复的错误
❌ 不使用 panic 的场景:
- 可预期的错误(文件不存在、网络超时)
- 用户输入验证
- 库函数中(应返回 error)
12.7 错误处理最佳实践
package main
import (
"errors"
"fmt"
"log"
)
// ✅ 及时处理错误
func good() error {
f, err := os.Open("file.txt")
if err != nil {
return err // 立即返回
}
defer f.Close()
// ...
return nil
}
// ✅ 错误信息应该添加上下文
func wrapError() error {
err := doOperation()
if err != nil {
return fmt.Errorf("failed to process user %d: %w", userID, err)
}
return nil
}
// ✅ 使用 errors.Is 比较,不要用字符串比较
func checkErr(err error) bool {
return errors.Is(err, ErrNotFound) // ✅
// return err.Error() == "not found" // ❌
}
// ✅ 在边界处处理错误(如 main、HTTP handler)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
// 所有错误传播到此处统一处理
cfg, err := loadConfig()
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
db, err := connectDB(cfg)
if err != nil {
return fmt.Errorf("connecting to DB: %w", err)
}
return startServer(db)
}
12.8 Go 1.20+ errors.Join
// 合并多个错误
func validate(name string, age int) error {
var errs []error
if name == "" {
errs = append(errs, errors.New("name is required"))
}
if age < 0 {
errs = append(errs, errors.New("age must be positive"))
}
return errors.Join(errs...)
}
func main() {
err := validate("", -1)
if err != nil {
fmt.Println(err)
// name is required
// age must be positive
// errors.Is 可以检查任一错误
fmt.Println(errors.Is(err, errors.New("name is required"))) // false(新创建的错误)
}
}
🏢 业务场景
- API 错误码:自定义错误类型包含 HTTP 状态码
- 数据库操作:包装 SQL 错误为业务错误
- 配置验证:累积多个验证错误一次性返回
- 中间件恢复:defer+recover 捕获 panic,防止服务崩溃
- 重试机制:判断错误类型决定是否可重试
📖 扩展阅读