2026-06-10
go
0

目录

Go 错误处理 — 完整学习手册
目录
1. 核心思想:Error is a Value
跟其他语言的对比
2. 创建错误的三种方式
方式一:errors.New(最简单)
方式二:fmt.Errorf(最常用)
方式三:自定义结构体(最灵活)
3. 哨兵错误(Sentinel Error)
定义
使用
为什么不用 == 而用 errors.Is?
标准库的哨兵错误
4. 自定义错误类型
基本实现
使用示例
带堆栈的自定义错误(生产级)
5. 错误包装(Wrapped Error)
包装 vs 不包装
多层包装
错误链的可视化
6. errors.Is vs errors.As
errors.Is — 查找是否包含特定错误值
errors.As — 查找并提取特定错误类型
Is vs As 对比
同时使用
7. panic 和 recover
panic 不是 throw
什么时候应该 panic?
recover 怎么用?
完整的 panic → error 模式
🚨 panic 黄金法则
8. defer 中的错误处理
基础:defer 与错误
进阶:捕获 defer 的 error
命名返回值的妙用
9. 生产级错误设计实战
完整示例:三层架构的错误设计
step 1:定义错误码
step 2:定义错误结构体
step 3:各层使用
优势总结
10. 常见陷阱总结
陷阱 1:%v vs %w
陷阱 2:用 == 比较包装后的错误
陷阱 3:直接把 error 当 bool 用
陷阱 4:忽略 defer 中的错误
陷阱 5:用 panic 做流程控制
陷阱 6:错误类型的值接收者 vs 指针接收者
陷阱 7:switch 中的错误比较
11. 练习题
练习 1:实现自定义错误包
练习 2:重构错误处理
练习 3:三层错误传递
练习 4:defer 错误捕获
最终速查表

Go 错误处理 — 完整学习手册

目录


1. 核心思想:Error is a Value

Go 没有 try-catch。错误就是一个,跟 int、string 一样可以被:

  • 返回
  • 传递
  • 比较
  • 存储
go
type error interface { Error() string }

只要实现了 Error() string 就算是一个 error。

跟其他语言的对比

语言错误处理方式特点
Java/Pythontry-catch 异常调用链自动展开,可能被忽略
Goerror 作为返回值显式处理,不会漏掉
RustResult<T, E>类似 Go 的 (T, error)

Go 的哲学: 错误是正常流程的一部分,不是意外。所以你要显式处理它。


2. 创建错误的三种方式

方式一:errors.New(最简单)

go
import "errors" var ErrNotFound = errors.New("item not found")

errors.New 底层就是返回一个 *errorString

go
func New(text string) error { return &errorString{text} } type errorString struct { s string } func (e *errorString) Error() string { return e.s }

特点: 包级变量,全局唯一,调用方可以用 == 比较。

方式二:fmt.Errorf(最常用)

go
err := fmt.Errorf("user %d not found: %w", userID, ErrNotFound)

%w 创建 wrapped error(见第 5 节),%v 只生成字符串不保留链。

方式三:自定义结构体(最灵活)

go
type ValidationError struct { Field string Value interface{} Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) }

见第 4 节详细展开。


3. 哨兵错误(Sentinel Error)

"哨兵"这个名字来自于 C 语言中表示边界的特殊值。这里指用特定变量标识一种已知错误

定义

go
package errors import "errors" var ( ErrNotFound = errors.New("resource not found") ErrPermission = errors.New("permission denied") ErrTimeout = errors.New("operation timed out") ErrInvalidInput = errors.New("invalid input") )

通常放在一个独立的包(比如 pkg/errors/internal/errors/)里。

使用

go
func GetUserByID(id int64) (*User, error) { row := db.QueryRow("SELECT * FROM users WHERE id = ?", id) var u User err := row.Scan(&u.Name, &u.Email) if err == sql.ErrNoRows { return nil, ErrNotFound // 返回哨兵 } return &u, nil } // 调用方 user, err := GetUserByID(42) if errors.Is(err, ErrNotFound) { // 处理"找不到"的情况 } else if err != nil { // 处理其他错误 }

为什么不用 == 而用 errors.Is?

go
// ❌ 不推荐 if err == ErrNotFound { ... } // ✅ 推荐 if errors.Is(err, ErrNotFound) { ... }

因为 %w 包装后的错误不再是原来的哨兵,但它们包含原哨兵:

go
err := fmt.Errorf("查询用户失败: %w", ErrNotFound) // err != ErrNotFound ✅ 成立 // errors.Is(err, ErrNotFound) ✅ 也成立

所以 errors.Is 会沿着错误链递归查找,不管包装了多少层。

标准库的哨兵错误

go
sql.ErrNoRows // 查询无结果 io.EOF // 读到文件尾 io.ErrUnexpectedEOF // 意外读完 context.Canceled // context 被取消 context.DeadlineExceeded // 超时

4. 自定义错误类型

基本实现

go
type AppError struct { Code int // 业务错误码 Message string // 人类可读消息 Err error // 原始错误(可选) } // 实现 error 接口 func (e *AppError) Error() string { if e.Err != nil { return fmt.Sprintf("code=%d: %s (cause: %v)", e.Code, e.Message, e.Err) } return fmt.Sprintf("code=%d: %s", e.Code, e.Message) } // 实现 Unwrap,支持 errors.Is/As 链式查找 func (e *AppError) Unwrap() error { return e.Err }

使用示例

go
// 创建 func processOrder(id int) error { if id <= 0 { return &AppError{ Code: 400, Message: "invalid order id", Err: fmt.Errorf("received id=%d", id), } } // ... } // 调用方提取 var appErr *AppError if errors.As(err, &appErr) { // 拿到了 appErr,可以读 Code 和 Message fmt.Printf("业务错误 %d: %s\n", appErr.Code, appErr.Message) } // 或者直接 switch var appErr *AppError if errors.As(err, &appErr) { switch appErr.Code { case 400: // 参数错误 case 401: // 未认证 } }

带堆栈的自定义错误(生产级)

go
type ErrorWithStack struct { Err error Stack string } func (e *ErrorWithStack) Error() string { return e.Err.Error() } func (e *ErrorWithStack) Unwrap() error { return e.Err } func New(msg string) error { return &ErrorWithStack{ Err: errors.New(msg), Stack: getStackTrace(), } } func getStackTrace() string { // runtime.Caller 获取调用栈 var buf [1024]byte n := runtime.Stack(buf[:], false) return string(buf[:n]) }

5. 错误包装(Wrapped Error)

Go 1.13 引入了 %w,可以在错误周围"包裹"上下文,同时保留原始错误。

包装 vs 不包装

go
// ❌ 不包装:丢失原始错误 func LoadConfig() error { err := readFile("config.yaml") if err != nil { return fmt.Errorf("加载配置失败: %v", err) // %v 只把 err 转成字符串,调用方拿不到原始错误了 } } // ✅ 包装:保留原始错误 func LoadConfig() error { err := readFile("config.yaml") if err != nil { return fmt.Errorf("加载配置失败: %w", err) // %w 保留了原始错误,errors.Is/As 可以穿透 } } // 调用方 err := LoadConfig() if errors.Is(err, os.ErrNotExist) { // %v: ❌ 穿透不了 // %w: ✅ 能穿透,找到 os.ErrNotExist }

多层包装

go
// 三层嵌套的错误链 func readFile(path string) error { return os.ErrNotExist // 最底层 } func LoadConfig() error { err := readFile("config.yaml") if err != nil { return fmt.Errorf("加载配置失败: %w", err) // 中间层 } } func StartServer() error { err := LoadConfig() if err != nil { return fmt.Errorf("启动服务失败: %w", err) // 最外层 } } // 调用方 err := StartServer() if errors.Is(err, os.ErrNotExist) { // ✅ 穿透 3 层包装,找到 os.ErrNotExist }

错误链的可视化

StartServer() 返回: "启动服务失败: 加载配置失败: file does not exist" ↑ ↑ ↑ fmt.Errorf(%w) fmt.Errorf(%w) os.ErrNotExist (最外层) (中间层) (最底层)

errors.Is 从最外层往里递归遍历,调用每个 Unwrap() 直到找到目标或 nil。


6. errors.Is vs errors.As

这两个函数是 Go 1.13 引入的,用来在错误链中查找特定错误。

errors.Is — 查找是否包含特定错误值

go
// 定义 func Is(err, target error) bool // 判断 err 是否包含 target(沿着错误链查找)

查找逻辑:

  1. err == target → true
  2. err 实现了 Is(error) bool → 调这个方法
  3. err 实现了 Unwrap() error → 递归
  4. err 实现了 Unwrap() []error(Go 1.20+)→ 遍历每个分支

使用场景: 检查是否"是某种错误"

go
if errors.Is(err, ErrNotFound) { // 是"找不到"错误 } if errors.Is(err, io.EOF) { // 是"读到文件尾"错误 } if errors.Is(err, context.DeadlineExceeded) { // 是"超时"错误 }

errors.As — 查找并提取特定错误类型

go
// 定义 func As(err error, target interface{}) bool // target 必须是指向某 error 类型的指针 // 如果找到,会把错误赋值给 target,返回 true

使用场景: 需要获取错误的额外字段

go
// 定义了一个带 Code 的错误类型 type HTTPError struct { Code int Message string } func (e *HTTPError) Error() string { return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message) } // 调用方 var httpErr *HTTPError if errors.As(err, &httpErr) { // 拿到了原始的 *HTTPError fmt.Printf("HTTP status: %d\n", httpErr.Code) if httpErr.Code == 401 { // 需要重新登录 } }

Is vs As 对比

errors.Iserrors.As
查找目标值(哨兵)类型(结构体)
目标类型error 值error 类型的指针 (e.g. *MyError)
典型场景errors.Is(err, ErrNotFound)errors.As(err, &myErr)
匹配方式按值比较按类型赋值
多层包装✅ 递归 Unwrap✅ 递归 Unwrap

同时使用

go
// 先判断大类,再提取详情 if errors.Is(err, ErrValidation) { var validErr *ValidationError if errors.As(err, &validErr) { fmt.Printf("字段 %s 验证失败: %s", validErr.Field, validErr.Message) } }

7. panic 和 recover

panic 不是 throw

panic 不等于其他语言的 throw/raise。 panic 意味着程序遇到了不可恢复的错误。

什么时候应该 panic?

✅ 适合 panic 的场景:

go
// 1. 程序初始化失败(启动时) func init() { lis, err := net.Listen("tcp", ":8080") if err != nil { panic(fmt.Sprintf("端口 8080 被占用: %v", err)) // 启动不了的服务器没有意义 } _ = lis } // 2. 不可恢复的内部错误 func (s *Server) Start() { if s == nil { panic("nil pointer receiver") } // ... }

❌ 不适合 panic 的场景:

go
// 输入校验失败 → 返回 error func GetUser(id int) (*User, error) { if id <= 0 { return nil, ErrInvalidID // ✅ return error // panic("invalid id") ❌ 不能用 panic } }

recover 怎么用?

recover 只在 defer 中 有效:

go
func SafeCall(fn func()) (err error) { defer func() { if r := recover(); r != nil { // 把 panic 转成 error 返回 err = fmt.Errorf("panic: %v", r) } }() fn() return nil }

完整的 panic → error 模式

go
func safeExecute(ctx context.Context, fn func() error) (err error) { defer func() { if r := recover(); r != nil { // 从 panic 中恢复 const size = 64 << 10 // 64KB buf := make([]byte, size) buf = buf[:runtime.Stack(buf, false)] err = fmt.Errorf("panic recovered: %v\nstack:\n%s", r, buf) // 记录到日志 log.Printf("panic recovered: %v\n%s", r, buf) } }() return fn() } // 使用 err := safeExecute(ctx, func() error { // 这里面的 panic 都会被捕获 return someRiskyOperation() })

🚨 panic 黄金法则

if 程序还能继续 → return error if 程序必须退出 → panic(或 log.Fatal)

8. defer 中的错误处理

基础:defer 与错误

go
func ReadFile(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() // 一行搞定,但 Close 的错误被忽略了 return io.ReadAll(f) }

大多数场景下这样够了。但要更严谨:

进阶:捕获 defer 的 error

go
func ReadFileSafe(path string) (data []byte, err error) { f, err := os.Open(path) if err != nil { return nil, err } // 在 defer 中捕获 Close 的错误 defer func() { if cerr := f.Close(); cerr != nil { // 如果函数本身没有返回错误,则用 Close 的错误 if err == nil { err = cerr } // 如果函数已经有错误了,Close 的错误可以打 log 但不要覆盖 } }() data, err = io.ReadAll(f) return }

命名返回值的妙用

go
func CopyFile(src, dst string) (written int64, err error) { srcF, err := os.Open(src) if err != nil { return } defer srcF.Close() dstF, err := os.Create(dst) if err != nil { return } defer func() { if cerr := dstF.Close(); cerr != nil { if err == nil { err = cerr } } }() written, err = io.Copy(dstF, srcF) return }

关键:defer 中通过闭包修改了命名返回值 err


9. 生产级错误设计实战

完整示例:三层架构的错误设计

HTTP层(handler)→ Service层 → Repository层(DB)

step 1:定义错误码

go
// pkg/ecode/ecode.go package ecode type Code int const ( // 通用错误 Success Code = 0 Unknown Code = -1 InternalError Code = 500 // 业务错误 (4xx) BadRequest Code = 400 NotFound Code = 404 AlreadyExists Code = 409 ValidationError Code = 422 // 认证错误 Unauthorized Code = 401 Forbidden Code = 403 ) func (c Code) Message() string { switch c { case Success: return "success" case NotFound: return "resource not found" case ValidationError: return "validation failed" default: return "unknown error" } }

step 2:定义错误结构体

go
// internal/errors/errors.go package errors import ( "fmt" "runtime" "myapp/pkg/ecode" ) type AppError struct { Code ecode.Code Message string Err error // 原始错误(内部使用) Stack string // 错误发生位置的栈 } 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 New(code ecode.Code, msg string) *AppError { return &AppError{ Code: code, Message: msg, Stack: captureStack(2), } } func Wrap(err error, code ecode.Code, msg string) *AppError { return &AppError{ Code: code, Message: msg, Err: err, Stack: captureStack(2), } } func captureStack(skip int) string { const depth = 32 var pcs [depth]uintptr n := runtime.Callers(skip+1, pcs[:]) frames := runtime.CallersFrames(pcs[:n]) var stack string for { frame, more := frames.Next() stack += fmt.Sprintf("\n %s\n %s:%d", frame.Function, frame.File, frame.Line) if !more { break } } return stack } // 判断是否是 4xx 客户端错误 func IsClientError(err error) bool { var appErr *AppError if errors.As(err, &appErr) { return appErr.Code >= 400 && appErr.Code < 500 } return false }

step 3:各层使用

go
// Repository 层:返回原始错误或哨兵 type UserRepo struct { db *sql.DB } func (r *UserRepo) FindByID(ctx context.Context, id int64) (*User, error) { var u User err := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id). Scan(&u.Name, &u.Email) if errors.Is(err, sql.ErrNoRows) { return nil, sql.ErrNoRows // 返回哨兵 } return &u, nil } // Service 层:包装为业务错误 type UserService struct { repo *UserRepo } func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) { if id <= 0 { return nil, apperrors.New(ecode.BadRequest, "invalid user id") } user, err := s.repo.FindByID(ctx, id) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, apperrors.Wrap(err, ecode.NotFound, "user not found") } return nil, apperrors.Wrap(err, ecode.InternalError, "query user failed") } return user, nil } // Handler 层:将错误转为 HTTP 响应 type UserHandler struct { svc *UserService } func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64) user, err := h.svc.GetUser(r.Context(), id) if err != nil { var appErr *apperrors.AppError if errors.As(err, &appErr) { // 给调用方返回结构化的错误 w.WriteHeader(httpStatusCode(appErr.Code)) json.NewEncoder(w).Encode(map[string]interface{}{ "code": appErr.Code, "message": appErr.Message, }) } else { w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]interface{}{ "code": ecode.InternalError, "message": "internal error", }) } return } json.NewEncoder(w).Encode(user) }

优势总结

这种设计的好处:

场景RepositoryServiceHandler
用户不存在返回 sql.ErrNoRows包装为 NotFoundException返回 404 JSON
DB 挂了返回 driver.ErrBadConn包装为 InternalError返回 500 JSON
参数错误返回 BadRequestError返回 400 JSON

每层只关注自己该关注的,错误像洋葱一样层层包裹。


10. 常见陷阱总结

陷阱 1:%v vs %w

go
// ❌ 丢失原始错误 return fmt.Errorf("something: %v", err) // ✅ 保留错误链 return fmt.Errorf("something: %w", err)

陷阱 2:用 == 比较包装后的错误

go
err := fmt.Errorf("wrap: %w", ErrNotFound) // ❌ 不成立 if err == ErrNotFound { ... } // ✅ 成立 if errors.Is(err, ErrNotFound) { ... }

陷阱 3:直接把 error 当 bool 用

go
// ❌ 反直觉 if err { // ... } // ✅ Go 的习惯 if err != nil { // ... }

陷阱 4:忽略 defer 中的错误

go
f, _ := os.Open("file") defer f.Close() // Close 的错误被静默丢弃了 // 可能导致数据没写全但你不知道

陷阱 5:用 panic 做流程控制

go
// ❌ 把异常当流程控制用 func parseInt(s string) (int, error) { defer func() { if r := recover(); r != nil { // 从 strconv 的 panic 中恢复?别这么干 } }() return strconv.Atoi(s) // 实际上 Atoi 返回 error,不 panic } // ✅ 正确处理 func parseInt(s string) (int, error) { return strconv.Atoi(s) }

陷阱 6:错误类型的值接收者 vs 指针接收者

go
type MyError struct { Msg string } // ✅ 指针接收者才能被 errors.As 匹配 func (e *MyError) Error() string { return e.Msg } // ❌ 值接收者 func (e MyError) Error() string { return e.Msg } // errors.As(&myErr) 需要指针

陷阱 7:switch 中的错误比较

go
// ❌ 如果没有 errors.Is,只能==比较 switch err { case ErrNotFound: // ... case ErrPermission: // ... } // ✅ 用 errors.Is switch { case errors.Is(err, ErrNotFound): // ... case errors.Is(err, ErrPermission): // ... }

11. 练习题

练习 1:实现自定义错误包

创建一个 myerrors 包,要求:

go
// 必须支持以下用法 func Process(id int) error { if id <= 0 { return myerrors.New(400, "invalid id"). WithField("id", id) } return nil } // 调用方 var bizErr *myerrors.BizError if errors.As(err, &bizErr) { fmt.Println(bizErr.Code, bizErr.Fields) }

包含:

  • 错误码(int)
  • 错误消息(string)
  • 额外字段(map[string]interface{})
  • 原始错误(error)
  • StackTrace
go
package myerrors import ( "fmt" "runtime" ) type BizError struct { Code int Msg string Err error Fields map[string]any Stack string } func getStackTrace() string { pc := make([]uintptr, 32) n := runtime.Callers(3, pc) // 跳过 Callers 自身 + getStackTrace + New frames := runtime.CallersFrames(pc[:n]) var sb strings.Builder for { frame, more := frames.Next() fmt.Fprintf(&sb, "%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line) if !more { break } } return sb.String() } func New(code int, msg string) *BizError { bz := &BizError{ Code: code, Msg: msg, Err: nil, Fields: make(map[string]any, 10), Stack: getStackTrace(), } return bz } func (bz *BizError) Error() string { if bz.Err != nil { return fmt.Sprintf("Code=%d: %s (cause %v)", bz.Code, bz.Msg, bz.Err) } return fmt.Sprintf("Code=%d: %s", bz.Code, bz.Msg) } func (bz *BizError) Unwrap() error { return bz.Err } func (bz *BizError) WithField(key string, value any) *BizError { bz.Fields[key] = value return bz }

练习 2:重构错误处理

go
// 这段代码的问题在哪里?重构它。 func GetOrder(id int) (*Order, error) { rows, err := db.Query("SELECT * FROM orders WHERE id = ?", id) if err != nil { return nil, err // 原始错误直接返回给了调用方 } defer rows.Close() if !rows.Next() { return nil, errors.New("not found") // 每次 new,不能 == 比较 } var o Order err = rows.Scan(&o.ID, &o.Amount) if err != nil { panic(err) // 啊?panic? } return &o, nil }
go
var ErrOrderNotFound = errors.New("order not found") func GetOrder(id int) (*Order, error) { rows, err := db.Query("SELECT * FROM orders WHERE id = ?", id) if err != nil { return nil, fmt.Errorf("Query Order %d: %w", id, err) } defer func() { if cerr := rows.Close(); cerr != nil && err == nil { err = cerr } }() if !rows.Next() { return nil, fmt.Errorf("order %d: %w", id, ErrOrderNotFound) // 每次 new,不能 == 比较 } var o Order if err = rows.Scan(&o.ID, &o.Amount); err != nil { return nil, fmt.Errorf("Scan Order %d: %w", id, err) // 啊?panic? } if err := rows.Err(); err != nil { return nil, fmt.Errorf("rows iteration order %d: %w", id, err) } return &o, nil }

练习 3:三层错误传递

写程序模拟:

  1. Handler 层调用 Service 层
  2. Service 层调用 Repository 层
  3. Repository 层返回哨兵错误
  4. Service 层包装
  5. Handler 层根据 Code 返回不同 HTTP 状态码
go
package main import ( "errors" "fmt" "log" "net/http" ) // ============================================================ // 1. Domain model // ============================================================ type User struct { ID int `json:"id"` Name string `json:"name"` } // ============================================================ // 2. Repository layer — 哨兵错误定义在这里 // ============================================================ var ( ErrUserNotFound = errors.New("user not found") ErrUserDisabled = errors.New("user disabled") ErrDatabaseDown = errors.New("database is down") ErrDuplicateEmail = errors.New("duplicate email") ) // UserRepository 接口(便于测试和切换实现) type UserRepository interface { FindByID(id int) (*User, error) Save(u *User) error } // userRepo 模拟实现 type userRepo struct { store map[int]*User } func NewUserRepository() UserRepository { return &userRepo{ store: map[int]*User{ 1: {ID: 1, Name: "Alice"}, // ID=2 故意缺失,模拟"未找到" // ID=3 的 user disabled 3: {ID: 3, Name: "Bob"}, }, } } func (r *userRepo) FindByID(id int) (*User, error) { // 模拟数据库宕机 if id == 999 { return nil, ErrDatabaseDown } u, ok := r.store[id] if !ok { return nil, ErrUserNotFound } if u.ID == 3 { return nil, ErrUserDisabled } return u, nil } func (r *userRepo) Save(u *User) error { // 模拟重复邮箱 if u.ID == 1 { return fmt.Errorf("save user: %w", ErrDuplicateEmail) } return nil } // ============================================================ // 3. Service layer — 将哨兵错误包装成业务错误码 // ============================================================ // ErrorCode 业务错误码枚举 type ErrorCode int const ( CodeOK ErrorCode = 0 CodeNotFound ErrorCode = 1001 CodeUserDisabled ErrorCode = 1002 CodeDatabaseError ErrorCode = 1003 CodeDuplicateEmail ErrorCode = 1004 CodeInternalError ErrorCode = 9999 ) // AppError 业务层统一错误结构 type AppError struct { Code ErrorCode `json:"code"` Message string `json:"message"` Err error `json:"-"` // 原始错误,不暴露给客户端 } // Error 实现 error 接口 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) } // Unwrap 支持 errors.Is / errors.As 遍历错误链 func (e *AppError) Unwrap() error { return e.Err } // WrapError 将 repository 的哨兵错误包装为业务 AppError func WrapError(err error) error { if err == nil { return nil } // 如果已经是 AppError,直接返回 var appErr *AppError if errors.As(err, &appErr) { return err } // 根据底层哨兵错误映射到业务错误码 switch { case errors.Is(err, ErrUserNotFound): return &AppError{ Code: CodeNotFound, Message: "请求的资源不存在", Err: err, } case errors.Is(err, ErrUserDisabled): return &AppError{ Code: CodeUserDisabled, Message: "用户已被禁用", Err: err, } case errors.Is(err, ErrDatabaseDown): return &AppError{ Code: CodeDatabaseError, Message: "数据库暂时不可用", Err: err, } case errors.Is(err, ErrDuplicateEmail): return &AppError{ Code: CodeDuplicateEmail, Message: "邮箱已被注册", Err: err, } default: return &AppError{ Code: CodeInternalError, Message: "服务器内部错误", Err: err, } } } // UserService 业务接口 type UserService interface { GetUser(id int) (*User, error) CreateUser(u *User) error } type userService struct { repo UserRepository } func NewUserService(repo UserRepository) UserService { return &userService{repo: repo} } // GetUser — Service 层调用 Repository,并包装错误 func (s *userService) GetUser(id int) (*User, error) { user, err := s.repo.FindByID(id) if err != nil { // 用 %w 保留原始错误链,再用 WrapError 包装为 AppError return nil, WrapError(fmt.Errorf("get user %d: %w", id, err)) } return user, nil } func (s *userService) CreateUser(u *User) error { if err := s.repo.Save(u); err != nil { return WrapError(fmt.Errorf("create user: %w", err)) } return nil } // ============================================================ // 4. Handler layer — 根据 AppError.Code 返回不同 HTTP 状态码 // ============================================================ // errorToHTTPStatus 映射业务错误码 → HTTP 状态码 func errorToHTTPStatus(code ErrorCode) int { switch code { case CodeNotFound: return http.StatusNotFound // 404 case CodeUserDisabled: return http.StatusForbidden // 403 case CodeDuplicateEmail: return http.StatusConflict // 409 case CodeDatabaseError: return http.StatusServiceUnavailable // 503 case CodeInternalError: return http.StatusInternalServerError // 500 default: return http.StatusInternalServerError } } // UserHandler HTTP handler type UserHandler struct { svc UserService } func NewUserHandler(svc UserService) *UserHandler { return &UserHandler{svc: svc} } // GetUserHandler 处理 GET /user?id=xxx func (h *UserHandler) GetUserHandler(w http.ResponseWriter, r *http.Request) { // 从查询参数解析 id id := 1 // 默认 if s := r.URL.Query().Get("id"); s != "" { fmt.Sscanf(s, "%d", &id) } user, err := h.svc.GetUser(id) if err != nil { // 统一错误处理:提取 AppError → 返回对应 HTTP 状态码 + JSON body var appErr *AppError if errors.As(err, &appErr) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(errorToHTTPStatus(appErr.Code)) fmt.Fprintf(w, `{"code":%d,"message":"%s"}`, appErr.Code, appErr.Message) log.Printf("[Handler] 返回错误: code=%d, msg=%s, original=%v", appErr.Code, appErr.Message, appErr.Err) } else { // 非 AppError 兜底(理论上不会走到这里) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, `{"code":9999,"message":"服务器内部错误"}`) log.Printf("[Handler] 未知错误类型: %v", err) } return } // 成功返回 w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `{"id":%d,"name":"%s"}`, user.ID, user.Name) } // ============================================================ // 5. main — 启动模拟服务器并打印测试说明 // ============================================================ func main() { // 组装依赖链:Handler → Service → Repository repo := NewUserRepository() svc := NewUserService(repo) handler := NewUserHandler(svc) http.HandleFunc("/user", handler.GetUserHandler) addr := ":8080" fmt.Printf("模拟服务器启动在 http://localhost%s\n\n", addr) fmt.Println("========== 测试用 URL ==========") fmt.Println(" GET /user?id=1 → 200 OK(Alice)") fmt.Println(" GET /user?id=2 → 404 Not Found") fmt.Println(" GET /user?id=3 → 403 Forbidden") fmt.Println(" GET /user?id=999 → 503 Service Unavailable") fmt.Println("================================") if err := http.ListenAndServe(addr, nil); err != nil { log.Fatalf("服务器启动失败: %v", err) } }

练习 4:defer 错误捕获

go
// 实现一个 WriteFile 函数 // 1. 创建文件 // 2. 写入数据 // 3. 在 defer 中 Close // 4. 如果 Close 失败且 Write 没有失败,返回 Close 的错误 func WriteFile(path string, data []byte) (err error) { // 你的代码 }
go
// 实现一个 WriteFile 函数 // 1. 创建文件 // 2. 写入数据 // 3. 在 defer 中 Close // 4. 如果 Close 失败且 Write 没有失败,返回 Close 的错误 func WriteFile(path string, data []byte) (err error) { // 你的代码 file, err := os.Create(path) if err != nil { return err } defer func() { if cerr := file.Close(); cerr != nil && err == nil { err = cerr } }() buf := bufio.NewWriter(file) if _, berr := buf.Write(data); berr != nil { return } if ferr := buf.Flush(); ferr != nil { return } return }

最终速查表

go
// 📌 创建错误 errors.New("msg") // 最简单的哨兵 fmt.Errorf("wrap: %w", err) // 带上下文的包装 &AppError{Code: 400, ...} // 自定义类型 // 📌 检查错误 errors.Is(err, ErrNotFound) // 检查值(哨兵) errors.As(err, &myErr) // 提取类型(自定义) err != nil // 检查是否有错误 // 📌 包装和解包 fmt.Errorf("ctx: %w", err) // 包装(保留链) errors.Unwrap(err) // 解一层 // 📌 panic panic("fatal") // 只有不可恢复时用 defer func() { recover() } // 在 defer 中捕获 // 📌 黄金原则 // return error > panic // %w > %v when wrapping // errors.Is/As > ==