Go 从 0 到精通 · 第 16 课:错误处理 error
学习定位:这是整套 Go 教程的第 16 课。
前置要求:已经完成第 15 课,理解了组合优于继承的设计哲学,掌握了接口和嵌入的使用。
本课目标:掌握 Go 的显式错误处理机制,理解 error 接口,能创建、返回、检查和包装错误,写出清晰可靠的错误处理逻辑。
1. 本课你要解决的核心问题
你已经见过错误处理很多次了——第 13 课的银行系统示例中:
1 2 3
| if err := alice.Deposit(200); err != nil { fmt.Println("存款失败:", err) }
|
Go 没有 try-catch,没有异常机制(严格来说有,但不常用)。Go 的错误处理方式是:函数返回一个 error 值,调用者检查它。
你需要搞明白以下问题:
error 接口到底是什么
- 怎么创建错误
- 怎么返回和检查错误
- 错误怎么层层传递
- 什么时候用
panic(下一课讲),什么时候返回 error
- 错误处理的常见模式和最佳实践
学完这一课,你就能写出可靠的 Go 代码了。
2. error 接口
2.1 error 是一个接口
Go 标准库中的 error 接口定义非常简单:
1 2 3
| type error interface { Error() string }
|
只有一个方法:Error() string。任何实现了 Error() string 方法的类型都是一个 error。
2.2 判断错误是否为 nil
1 2 3 4 5 6 7
| result, err := someFunction() if err != nil { fmt.Println("错误:", err) return }
|
这是 Go 中最最常见的模式:函数的最后一个返回值是 error,nil 表示成功,非 nil 表示失败。
3. 创建错误
3.1 errors.New
1 2 3 4 5 6 7 8 9 10
| import "errors"
var ErrNotFound = errors.New("记录未找到")
func findUser(id int) (string, error) { if id <= 0 { return "", ErrNotFound } return "小明", nil }
|
errors.New 创建一个简单的错误值。同一个错误值可以复用。
3.2 fmt.Errorf
1 2 3 4 5 6 7 8
| import "fmt"
func findUser(id int) (string, error) { if id <= 0 { return "", fmt.Errorf("用户 %d 不存在", id) } return "小明", nil }
|
fmt.Errorf 创建带格式化信息的错误。适合需要包含动态信息的错误消息。
3.3 对比
| 方式 |
适用场景 |
示例 |
errors.New |
固定错误消息 |
errors.New("权限不足") |
fmt.Errorf |
需要动态信息 |
fmt.Errorf("用户 %d 不存在", id) |
4. 错误处理的基本模式
4.1 逐层检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package main
import ( "errors" "fmt" )
func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("除数不能为零") } return a / b, nil }
func main() { result, err := divide(10, 0) if err != nil { fmt.Println("计算失败:", err) return } fmt.Println("结果:", result) }
|
4.2 错误沿调用链传递
实际项目中,错误往往要层层传递:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| func readConfig(path string) (Config, error) { data, err := os.ReadFile(path) if err != nil { return Config{}, err } return config, nil }
func loadApp() error { config, err := readConfig("config.json") if err != nil { return err } return nil }
|
每一层检查错误,决定是处理还是传递。传递时可以原样传,也可以包装后传。
4.3 错误包装(wrapping)
Go 1.13 引入了 %w 动词来包装错误:
1 2 3 4 5 6 7 8
| func readConfig(path string) (Config, error) { data, err := os.ReadFile(path) if err != nil { return Config{}, fmt.Errorf("读取配置文件 %s 失败: %w", path, err) } return config, nil }
|
%w 把原始错误"包裹"在新错误中,保留了错误链。调用者可以用 errors.Is 和 errors.Unwrap 检查。
4.4 errors.Is 和 errors.As
1 2 3 4 5 6 7 8 9 10 11 12 13
| import ( "errors" "os" )
func main() { _, err := os.Open("不存在的文件.txt")
if errors.Is(err, os.ErrNotExist) { fmt.Println("文件不存在") } }
|
errors.Is 沿着错误链查找,判断是否匹配目标错误。
errors.As 用于提取特定类型的错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| type NotFoundError struct { Resource string ID int }
func (e *NotFoundError) Error() string { return fmt.Sprintf("%s #%d 未找到", e.Resource, e.ID) }
func main() { err := fmt.Errorf("查询失败: %w", &NotFoundError{Resource: "用户", ID: 42})
var nfe *NotFoundError if errors.As(err, &nfe) { fmt.Printf("资源: %s, ID: %d\n", nfe.Resource, nfe.ID) } }
|
5. 自定义错误类型
5.1 为什么需要自定义错误
errors.New 和 fmt.Errorf 创建的错误只有字符串信息。有时候需要携带更多上下文:
1 2 3 4 5 6 7 8 9
| type ValidationError struct { Field string Value string Message string }
func (e *ValidationError) Error() string { return fmt.Sprintf("字段 %s 的值 %q 无效: %s", e.Field, e.Value, e.Message) }
|
5.2 使用自定义错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| func validateAge(age int) error { if age < 0 { return &ValidationError{ Field: "age", Value: fmt.Sprintf("%d", age), Message: "年龄不能为负数", } } if age > 150 { return &ValidationError{ Field: "age", Value: fmt.Sprintf("%d", age), Message: "年龄不合理", } } return nil }
func main() { err := validateAge(-1) if err != nil { fmt.Println(err)
var ve *ValidationError if errors.As(err, &ve) { fmt.Printf("错误字段: %s\n", ve.Field) } } }
|
6. Sentinel Error(哨兵错误)
6.1 什么是哨兵错误
用包级别的变量定义的预定义错误叫"哨兵错误":
1 2 3 4 5
| package sql
import "errors"
var ErrNoRows = errors.New("sql: no rows in result set")
|
调用者可以用 errors.Is 检查:
1 2 3 4
| err := db.QueryRow("SELECT ...").Scan(&result) if errors.Is(err, sql.ErrNoRows) { }
|
6.2 命名规范
Go 社区约定:包级别的错误变量以 Err 开头:
1 2 3 4 5
| var ( ErrNotFound = errors.New("not found") ErrPermission = errors.New("permission denied") ErrTimeout = errors.New("timeout") )
|
7. 常见错误处理模式
7.1 快速失败
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| func process(data string) error { step1, err := parse(data) if err != nil { return fmt.Errorf("解析失败: %w", err) }
step2, err := validate(step1) if err != nil { return fmt.Errorf("验证失败: %w", err) }
err = save(step2) if err != nil { return fmt.Errorf("保存失败: %w", err) }
return nil }
|
每一步都检查错误,出错就立即返回。不要忽略错误。
7.2 收集多个错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| func validateAll(name, email, age string) error { var errs []string
if name == "" { errs = append(errs, "姓名不能为空") } if !strings.Contains(email, "@") { errs = append(errs, "邮箱格式不正确") }
if len(errs) > 0 { return fmt.Errorf("验证失败: %s", strings.Join(errs, "; ")) } return nil }
|
7.3 只处理一次错误
1 2 3 4 5 6 7 8 9 10 11 12
| result, err := doSomething() if err != nil { log.Println("错误:", err) return err }
result, err := doSomething() if err != nil { return fmt.Errorf("执行失败: %w", err) }
|
8. 一段综合示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| package main
import ( "errors" "fmt" "strconv" )
type FieldError struct { Field string Message string }
func (e *FieldError) Error() string { return fmt.Sprintf("字段 %q: %s", e.Field, e.Message) }
func validateName(name string) error { if name == "" { return &FieldError{Field: "name", Message: "不能为空"} } return nil }
func validateAge(ageStr string) (int, error) { age, err := strconv.Atoi(ageStr) if err != nil { return 0, fmt.Errorf("字段 \"age\" 不是有效数字: %w", err) } if age < 0 || age > 150 { return 0, &FieldError{Field: "age", Message: fmt.Sprintf("值 %d 不在合理范围", age)} } return age, nil }
func register(name string, ageStr string) error { if err := validateName(name); err != nil { return fmt.Errorf("注册失败: %w", err) }
age, err := validateAge(ageStr) if err != nil { return fmt.Errorf("注册失败: %w", err) }
fmt.Printf("注册成功: %s, %d岁\n", name, age) return nil }
func main() { fmt.Println("--- 正常注册 ---") if err := register("小明", "20"); err != nil { fmt.Println("错误:", err) }
fmt.Println("\n--- 姓名为空 ---") if err := register("", "20"); err != nil { fmt.Println("错误:", err)
var fe *FieldError if errors.As(err, &fe) { fmt.Printf("错误字段: %s\n", fe.Field) } }
fmt.Println("\n--- 年龄非法 ---") if err := register("小红", "abc"); err != nil { fmt.Println("错误:", err) }
fmt.Println("\n--- 年龄超范围 ---") if err := register("小刚", "200"); err != nil { fmt.Println("错误:", err) } }
|
输出:
1 2 3 4 5 6 7 8 9 10 11 12
| --- 正常注册 --- 注册成功: 小明, 20岁
--- 姓名为空 --- 错误: 注册失败: 字段 "name": 不能为空 错误字段: name
--- 年龄非法 --- 错误: 注册失败: 字段 "age" 不是有效数字: strconv.Atoi: parsing "abc": invalid syntax
--- 年龄超范围 --- 错误: 注册失败: 字段 "age": 值 200 不在合理范围
|
9. 常见坑总结
9.1 忽略错误
1 2
| result, _ := doSomething()
|
下划线 _ 忽略错误在极少数场景下是合理的,但大多数时候应该处理错误。
9.2 用 == 比较包装过的错误
1 2 3 4 5
| err == originalErr
errors.Is(err, originalErr)
|
9.3 返回零值和 nil
1 2 3 4 5 6
| func findUser(id int) (User, error) { if id <= 0 { return User{}, errors.New("invalid id") } }
|
如果返回值是结构体,错误时返回零值 User{},不要返回 nil。
9.4 用字符串判断错误类型
1 2 3 4 5
| if err.Error() == "not found" { ... }
if errors.Is(err, ErrNotFound) { ... }
|
错误消息可能变化,用 errors.Is 更可靠。
9.5 过度包装
1 2 3 4 5
| return fmt.Errorf("处理: %w", fmt.Errorf("执行: %w", fmt.Errorf("调用: %w", err)))
return fmt.Errorf("加载配置 %s 失败: %w", path, err)
|
10. 本课练习
练习 1:创建和返回错误
要求:
- 写一个函数
sqrt(n float64) (float64, error)
- 负数返回错误,非负数返回平方根
- 测试正常和异常情况
练习 2:自定义错误类型
要求:
- 定义
NetworkError 类型(含 StatusCode int 和 Message string)
- 写一个函数模拟 HTTP 请求,返回
NetworkError
- 用
errors.As 提取错误信息
练习 3:错误包装
要求:
- 写三层调用链:
main → loadApp → readConfig
readConfig 调用 os.ReadFile 并包装其错误
- 每层都添加上下文信息
- 用
errors.Is 检查原始错误类型
练习 4:哨兵错误
要求:
- 定义包级别的哨兵错误
ErrInsufficientFunds 和 ErrNegativeAmount
- 在银行账户的
Withdraw 和 Deposit 方法中使用
- 调用者用
errors.Is 判断具体错误类型
练习 5:表单验证
要求:
- 实现一个函数验证用户名(非空、长度 3-20)、邮箱(包含
@)、年龄(数字、0-150)
- 每个字段的错误用
FieldError 自定义类型
- 收集所有错误后一次性返回
11. 自测题
11.1 概念题
- Go 的
error 接口定义了什么方法?
nil error 表示什么?
errors.New 和 fmt.Errorf 有什么区别?
%w 动词在 fmt.Errorf 中做什么?
errors.Is 和 == 比较错误有什么区别?
errors.As 做什么?
- 什么是哨兵错误?
- 为什么不应该忽略错误?
- Go 的错误处理哲学和 try-catch 有什么不同?
- 什么时候应该包装错误?
12. 本课总结
这一课你学到了 Go 的核心错误处理机制。
你现在应该已经理解:
error 是一个只有 Error() string 方法的接口
- 用
errors.New 或 fmt.Errorf 创建错误
- 函数返回
error,调用者用 if err != nil 检查
- 用
%w 包装错误,保留错误链
- 用
errors.Is 判断特定错误,用 errors.As 提取错误类型
- 自定义错误类型携带更多上下文信息
- 错误处理的核心原则:不忽略、快失败、提供上下文
13. 下一课预告
下一课我们学习 Go 中处理异常流程的机制:defer、panic、recover。
会重点讲:
defer 做什么,执行顺序是什么
panic 什么时候触发
recover 怎么捕获 panic
- 什么时候该用
panic,什么时候该返回 error
学完下一课,你就能正确管理资源释放,处理不可恢复的错误了。