错误处理
第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 或自定义类型包装底层错误,保留上下文 |
错误处理流程图
函数返回 error
│
▼
err != nil ?
┌────┴────┐
│ Yes │ No
▼ ▼
处理错误 继续执行
│
├─ 可恢复 → 记录日志 / 返回默认值 / 重试
├─ 需上报 → 包装后向上传递
└─ 不可恢复 → panic
基本语法与用法
1. error 接口
error 是 Go 内置的接口类型,定义非常简洁:
// error 接口的定义(内置,无需导入)
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都是一个 error。
2. 创建错误
package main
import (
"errors"
"fmt"
)
func main() {
// 方式一:errors.New —— 创建简单文本错误
err1 := errors.New("文件不存在")
// 方式二:fmt.Errorf —— 创建格式化错误
filename := "config.yaml"
err2 := fmt.Errorf("读取文件 %s 失败", filename)
// 方式三:fmt.Errorf + %w —— 包装底层错误(推荐)
baseErr := errors.New("permission denied")
err3 := fmt.Errorf("无法写入日志: %w", baseErr)
fmt.Println(err1) // 文件不存在
fmt.Println(err2) // 读取文件 config.yaml 失败
fmt.Println(err3) // 无法写入日志: permission denied
}
3. 检查错误
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("记录未找到")
func findUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return "张三", 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("捕获到恐慌: %v", r)
}
}()
// 除以零会触发 panic
return a / b, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("错误:", err) // 错误: 捕获到恐慌: runtime error: integer divide by zero
return
}
fmt.Println("结果:", result)
}
error 处理,而不是 panic。只在程序确实无法继续运行时才使用 panic(如初始化失败、不可恢复的逻辑错误)。
fmt.Errorf 包装错误时,始终用 %w 而非 %v。%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: "张三",
},
}
svc := &Service{repo: repo}
// 测试场景
testCases := []struct {
name string
id int
requireAdmin bool
}{
{"查找admin", 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(" 数据库详情: 表=%s, 操作=%s\n", dbErr.Table, dbErr.Operation)
}
} else {
fmt.Printf(" 成功: 用户 %s\n", user)
}
fmt.Println()
}
}
运行结果:
--- 查找admin ---
成功: 用户 admin
--- 查找普通用户 ---
成功: 用户 张三
--- 查找不存在的用户 ---
失败: 获取用户信息(id=99): 数据库错误 [users.SELECT]: 用户不存在
处理: 提示用户检查ID
数据库详情: 表=users, 操作=SELECT
--- 普通用户访问管理功能 ---
失败: 用户 张三: 权限不足
处理: 提示用户联系管理员
示例:自定义错误类型与 recover 实战(难度⭐⭐⭐)
实现一个完整的 HTTP 请求处理器,包含自定义错误类型、错误码、panic 恢复中间件。
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 的 bug
if path == "/crash" {
var p *int
*p = 1 // 空指针解引用,触发 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
}
// ==================== Recovery 中间件 ====================
// 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
实际场景
场景一:数据库事务回滚
在涉及多个步骤的数据库操作中,任何一步失败都需要回滚已执行的操作。
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{
"张三": 1000,
"李四": 500,
"王五": 0,
},
frozen: map[string]bool{
"王五": true,
},
}
tests := []struct {
name string
from string
to string
amt float64
}{
{"正常转账", "张三", "李四", 200},
{"余额不足", "张三", "李四", 9999},
{"目标账户冻结", "张三", "王五", 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])
}
}
场景二:文件批量处理与错误收集
处理多个文件时,不应因一个文件失败而中断全部操作,而是收集所有错误后统一报告。
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: "处理",
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: "处理",
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", files[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)
}
}
}
❓ 常见问题
Q1: 什么时候该用 panic,什么时候该返回 error?
原则: 用 error 处理可预期的失败,用 panic 处理"不应该发生"的编程错误。
// ✓ 正确:返回 error —— 用户输入不可预期
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
// ✓ 正确:用 panic —— 这是程序员的 bug,不应该传入 nil
func MustNotNil(v interface{}) {
if v == nil {
panic("值不应为 nil,这是调用方的编程错误")
}
}
实际规则:
- 返回 error: 文件操作、网络请求、用户输入验证、业务逻辑失败
- 用 panic:
init()初始化失败、不可恢复的程序错误、测试中的t.Fatal - 永远不要: 用
panic处理普通的业务错误
Q2: 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 → "你是不是张三?"(判断身份)
// errors.As → "你是不是程序员?如果是,告诉我你的技能"(提取信息)
Q3: 为什么错误信息建议小写开头、不加标点?
Go 官方和社区的惯例:
// ✓ 推荐
fmt.Errorf("读取文件 %s 失败: %w", name, err)
// 输出: 读取文件 config.yaml 失败: permission denied
// ✗ 不推荐
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: connection refused
Q4: 如何比较被包装过的错误?
直接用 == 无法比较被包装的错误,必须用 errors.Is:
var ErrSentinel = errors.New("sentinel")
wrapped := fmt.Errorf("context: %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 错误处理的三条黄金法则:
- 立即检查 —— 拿到
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:实现错误收集器(并发安全)
编写一个 ErrorCollector 类型,用于并发场景中收集多个 goroutine 的错误:
type ErrorCollector struct {
// 你需要的字段
}
// Add 添加一个错误(并发安全)
func (c *ErrorCollector) Add(err error) { ... }
// Errors 返回所有收集到的错误
func (c *ErrorCollector) Errors() []error { ... }
// HasErrors 是否有错误
func (c *ErrorCollector) HasErrors() bool { ... }
// Error 将所有错误合并为一个 error
func (c *ErrorCollector) Error() error { ... }
要求:
- 使用
sync.Mutex或sync.RWMutex保证并发安全 - 编写测试:启动多个 goroutine 并发调用
Add,最后验证收集结果
下一课
完成本课后,请继续学习 第11课:包与模块,了解如何组织 Go 代码、管理依赖、以及发布自己的包。



