目录
- [🔵 Go 入门到精通:错误处理](#🔵 Go 入门到精通:错误处理)
-
- [15.1 Go的错误处理哲学](#15.1 Go的错误处理哲学)
-
- [15.1.1 核心理念](#15.1.1 核心理念)
- [15.1.2 与其他语言的对比](#15.1.2 与其他语言的对比)
- [15.1.3 为什么Go选择这种方式](#15.1.3 为什么Go选择这种方式)
- [15.2 error 接口](#15.2 error 接口)
-
- [15.2.1 error 的零值](#15.2.1 error 的零值)
- [15.2.2 基本使用模式](#15.2.2 基本使用模式)
- [15.2.3 标准模式](#15.2.3 标准模式)
- [15.3 创建错误](#15.3 创建错误)
-
- [15.3.1 errors.New](#15.3.1 errors.New)
- [15.3.2 fmt.Errorf](#15.3.2 fmt.Errorf)
- [15.3.3 错误创建方式对比](#15.3.3 错误创建方式对比)
- [15.4 错误包装(Error Wrapping)](#15.4 错误包装(Error Wrapping))
-
- [15.4.1 什么是错误包装](#15.4.1 什么是错误包装)
- [15.4.2 多层包装](#15.4.2 多层包装)
- [15.4.3 %w vs %v](#15.4.3 %w vs %v)
- [15.5 errors.Is 解包判断](#15.5 errors.Is 解包判断)
-
- [15.5.1 基本用法](#15.5.1 基本用法)
- [15.5.2 工作原理](#15.5.2 工作原理)
- [15.5.3 更多示例](#15.5.3 更多示例)
- [15.6 errors.As 类型提取](#15.6 errors.As 类型提取)
- [15.7 自定义错误类型](#15.7 自定义错误类型)
-
- [15.7.1 基础自定义错误](#15.7.1 基础自定义错误)
- [15.7.2 带分类的错误](#15.7.2 带分类的错误)
- [15.8 Sentinel Error 模式](#15.8 Sentinel Error 模式)
-
- [15.8.1 定义哨兵错误](#15.8.1 定义哨兵错误)
- [15.8.2 使用哨兵错误](#15.8.2 使用哨兵错误)
- [15.8.3 Sentinel Error 的优缺点](#15.8.3 Sentinel Error 的优缺点)
- [15.9 panic 与 recover](#15.9 panic 与 recover)
-
- [15.9.1 panic 是什么](#15.9.1 panic 是什么)
- [15.9.2 何时使用 panic](#15.9.2 何时使用 panic)
- [15.9.3 recover 是什么](#15.9.3 recover 是什么)
- [15.10 defer 中的 recover](#15.10 defer 中的 recover)
-
- [15.10.1 标准模式](#15.10.1 标准模式)
- [15.10.2 在goroutine中recover](#15.10.2 在goroutine中recover)
- [15.10.3 recover 的重要限制](#15.10.3 recover 的重要限制)
- [15.11 错误处理最佳实践](#15.11 错误处理最佳实践)
-
- [15.11.1 十大黄金法则](#15.11.1 十大黄金法则)
- [15.11.2 更多法则](#15.11.2 更多法则)
- [15.11.3 早返回 vs 深层嵌套](#15.11.3 早返回 vs 深层嵌套)
- [15.12 综合案例:文件处理服务](#15.12 综合案例:文件处理服务)
- [15.13 本章总结](#15.13 本章总结)
🔵 Go 入门到精通:错误处理
📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处
🧑💻 作者:布朗克168 | 系列:Go入门到精通 2026 · 第三阶段 · 核心进阶
15.1 Go的错误处理哲学
Go语言的错误处理方式与其他主流语言截然不同。它没有 try-catch ,也没有异常抛出机制(panic 并非异常替代品),而是将错误作为普通的返回值来处理。
15.1.1 核心理念
go
// Go的错误处理:显式、透明
f, err := os.Open("file.txt")
if err != nil {
// 处理错误
return fmt.Errorf("打开文件失败: %w", err)
}
defer f.Close()
🎯 Go哲学:"错误是值(Errors are values)。" 错误不是异常,不是特殊情况------它是正常的、预期的返回值之一。
15.1.2 与其他语言的对比
| 特性 | Go | Java | Python | Rust |
|---|---|---|---|---|
| 错误机制 | 返回值 | try-catch异常 | try-except异常 | Result枚举 |
| 错误传递 | 显式逐层返回 | 异常冒泡 | 异常冒泡 | ? 传播 |
| 性能开销 | 低(值传递) | 高(栈展开) | 高(栈展开) | 低(枚举) |
| 强制处理 | ✅ 编译器不强制但lint可查 | ❌ 不检查异常 | ❌ 不检查异常 | ✅ 编译器强制 |
| 可读性 | 显式,有时啰嗦 | 隐式,可能吞异常 | 隐式 | 显式+语法糖 |
15.1.3 为什么Go选择这种方式
1. 显式 > 隐式:错误处理路径一目了然
2. 简单的错误处理不应比"正常"代码更复杂
3. 强制程序员面对每个可能的错误
4. 性能:异常机制在Go中代价太高(panic需要栈展开)
15.2 error 接口
Go的 error 是一个内置接口,简单得令人惊讶:
go
// 来自 builtin 包
type error interface {
Error() string
}
只有一个方法 Error() string 。任何实现了 Error() string 的类型都是一个错误。
15.2.1 error 的零值
go
var err error
fmt.Println(err == nil) // true(零值是 nil)
15.2.2 基本使用模式
go
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil // nil 表示没有错误
}
// 调用方必须检查错误
result, err := divide(10, 0)
if err != nil {
fmt.Println("计算出错:", err)
return
}
fmt.Println("结果:", result)
15.2.3 标准模式
go
// Go代码中最常见的模式
func DoSomething() error {
result, err := step1()
if err != nil {
return err
}
err = step2(result)
if err != nil {
return fmt.Errorf("step2失败: %w", err)
}
return nil
}
15.3 创建错误
Go提供了多种创建错误的方式,适用于不同场景。
15.3.1 errors.New
go
import "errors"
// 创建简单错误
var ErrNotFound = errors.New("记录未找到")
var ErrTimeout = errors.New("操作超时")
注意 :errors.New 创建的错误是不可变的,适合作为包级别的哨兵错误(sentinel error)。
15.3.2 fmt.Errorf
go
import "fmt"
// 格式化错误信息(支持参数)
err := fmt.Errorf("用户 %s 不存在 (ID: %d)", userName, userID)
// 带 %w 包裹另一个错误(Go 1.13+)
err := fmt.Errorf("查询用户失败: %w", originalErr)
15.3.3 错误创建方式对比
| 方式 | 适用场景 | 示例 |
|---|---|---|
errors.New("msg") |
简单固定错误 | ErrNotFound |
fmt.Errorf("fmt %s", arg) |
带动态信息的错误 | 参数化错误信息 |
fmt.Errorf("... %w", err) |
包裹(wrap)底层错误 | 添加上下文+保留原错误 |
| 自定义错误类型 | 需要携带额外信息 | 错误码、分类等 |
15.4 错误包装(Error Wrapping)
Go 1.13 引入的错误包装机制是错误处理的重要演进。
15.4.1 什么是错误包装
go
// 原始错误
err := sql.ErrNoRows
// 包装错误(添加更多上下文)
wrappedErr := fmt.Errorf("查询用户失败: %w", err)
fmt.Println(wrappedErr.Error())
// "查询用户失败: sql: no rows in result set"
关键 :%w 动词在 fmt.Errorf 中用来包装另一个错误,保留其"身份"。
15.4.2 多层包装
go
// 错误链:底层 → 中层 → 顶层
err1 := errors.New("数据库连接被拒绝") // 底层
err2 := fmt.Errorf("连接主库失败: %w", err1) // 中层
err3 := fmt.Errorf("初始化服务失败: %w", err2) // 顶层
fmt.Println(err3.Error())
// "初始化服务失败: 连接主库失败: 数据库连接被拒绝"
错误链示意图:
初始化服务失败 → 连接主库失败 → 数据库连接被拒绝
(err3) (err2) (err1)
↑ 外层 ↑ 中层 ↑ 底层(根因)
15.4.3 %w vs %v
| 动词 | 作用 | 能否用 errors.Is/errors.As 解包 |
|---|---|---|
%w |
包装错误,保留错误链 | ✅ 可以 |
%v |
仅格式化错误信息为字符串 | ❌ 不可以(断链) |
go
// ✅ 用 %w:保留错误链
wrapped := fmt.Errorf("失败: %w", originalErr)
errors.Is(wrapped, originalErr) // true
// ❌ 用 %v:断链
broken := fmt.Errorf("失败: %v", originalErr)
errors.Is(broken, originalErr) // false!
15.5 errors.Is 解包判断
errors.Is 用于判断错误链中是否包含某个特定错误。
15.5.1 基本用法
go
import "errors"
// 哨兵错误
var ErrNotFound = errors.New("not found")
func queryUser(id int) error {
return fmt.Errorf("查询用户 %d: %w", id, ErrNotFound)
}
func main() {
err := queryUser(42)
// ✅ errors.Is 沿着错误链查找
if errors.Is(err, ErrNotFound) {
fmt.Println("用户不存在")
}
// ❌ 不要直接用 == (只能判断最外层)
if err == ErrNotFound { // false!因为被包装了
// 不会执行
}
}
15.5.2 工作原理
errors.Is(err, target) 沿着错误链逐个比对:
1. err 自己 == target?
2. err.Unwrap() == target?(如果 err 实现了 Unwrap())
3. err.Unwrap().Unwrap() == target?
4. ... 直到链结束或找到匹配
15.5.3 更多示例
go
// 标准库的错误
_, err := os.Open("notexist.txt")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在,创建新文件")
}
// 对比多个错误
if errors.Is(err, io.EOF) {
fmt.Println("读取完毕")
} else if errors.Is(err, os.ErrPermission) {
fmt.Println("没有权限")
}
15.6 errors.As 类型提取
errors.As 用于从错误链中提取特定类型的错误。
15.6.1 基本用法
go
import "errors"
// 自定义错误类型
type ValidationError struct {
Field string
Value any
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("字段 %s 校验失败: %s", e.Field, e.Msg)
}
// 使用
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Value: age,
Msg: "年龄不在有效范围内",
}
}
return nil
}
func process() error {
err := validateAge(200)
if err != nil {
return fmt.Errorf("处理失败: %w", err)
}
return nil
}
func main() {
err := process()
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("校验失败!字段=%s, 值=%v, 原因=%s\n",
valErr.Field, valErr.Value, valErr.Msg)
}
}
15.6.2 errors.Is vs errors.As
| 维度 | errors.Is |
errors.As |
|---|---|---|
| 比较目标 | 具体值(sentinel error) | 类型 |
| 参数 | errors.Is(err, target) |
errors.As(err, &target) |
| 用途 | "是这个错误吗?" | "是这个类型的错误吗?" |
| 返回值 | bool |
bool |
| 提取信息 | ❌ 只能判断 | ✅ 可提取错误字段 |
| 示例 | errors.Is(err, io.EOF) |
errors.As(err, &pathErr) |
🔑 记忆口诀:"Is判断是不是这个错误,As提取是不是这种错误。"
15.7 自定义错误类型
当需要携带比字符串更多的错误信息时,自定义错误类型是最佳选择。
15.7.1 基础自定义错误
go
// 带错误码的错误
type AppError struct {
Code int
Message string
Cause error // 底层原因
}
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)
}
// 实现 Unwrap 方法,支持 errors.Is / errors.As
func (e *AppError) Unwrap() error {
return e.Cause
}
// 使用
func NewNotFoundError(msg string) *AppError {
return &AppError{Code: 404, Message: msg}
}
15.7.2 带分类的错误
go
type ErrorCategory int
const (
CategoryValidation ErrorCategory = iota + 1 // 校验错误
CategoryBusiness // 业务错误
CategorySystem // 系统错误
CategoryExternal // 外部依赖错误
)
type CategorizedError struct {
Category ErrorCategory
Message string
Cause error
}
func (e *CategorizedError) Error() string { return e.Message }
func (e *CategorizedError) Unwrap() error { return e.Cause }
// 根据分类做不同处理
func handleError(err error) {
var catErr *CategorizedError
if errors.As(err, &catErr) {
switch catErr.Category {
case CategoryValidation:
fmt.Println("⚠️ 客户端输入有误")
case CategorySystem:
fmt.Println("🔴 系统故障,触发告警")
case CategoryExternal:
fmt.Println("🟡 外部服务异常,启用降级")
}
}
}
15.8 Sentinel Error 模式
**Sentinel Error(哨兵错误)**是预定义的、包级别的错误值,调用方可以用 errors.Is 来判断。
15.8.1 定义哨兵错误
go
package userservice
import "errors"
// 包级别的哨兵错误
var (
ErrNotFound = errors.New("用户未找到")
ErrAlreadyExists = errors.New("用户已存在")
ErrInvalidEmail = errors.New("邮箱格式无效")
ErrUnauthorized = errors.New("未授权操作")
)
15.8.2 使用哨兵错误
go
// 服务层返回哨兵错误
func GetUser(id int) (*User, error) {
user, err := db.QueryUser(id)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound // 转换为业务哨兵错误
}
if err != nil {
return nil, fmt.Errorf("查询用户: %w", err)
}
return user, nil
}
// 调用方判断
user, err := GetUser(42)
if errors.Is(err, ErrNotFound) {
// 返回404
}
15.8.3 Sentinel Error 的优缺点
| 优点 | 缺点 |
|---|---|
| 简单直观 | 调用方产生对特定包的依赖 |
| 易于判断 | 无法携带动态信息 |
| 生态广泛使用 | 暴露实现细节 |
errors.Is 原生支持 |
错误信息固定 |
💡 建议 :哨兵错误适用于稳定的、为数不多的错误类型。动态信息用自定义错误类型。
15.9 panic 与 recover
panic 和 recover 是Go的"紧急机制",不是异常处理工具。
15.9.1 panic 是什么
go
func divide(a, b int) int {
if b == 0 {
panic("除数不能为零!") // 程序崩溃
}
return a / b
}
// 调用会导致:
// panic: 除数不能为零!
// goroutine 1 [running]:
// main.divide(...)
// main.main()
panic 会:
- 立即停止当前函数执行
- 执行所有已注册的
defer - 向调用栈上方传播
- 最终程序退出(如果没有
recover)
15.9.2 何时使用 panic
| ✅ 可以使用 panic | ❌ 不应用 panic |
|---|---|
| 程序无法继续运行的致命错误 | 正常的业务错误 |
| 编程错误(如数组越界) | 用户输入无效 |
| 初始化失败(init函数中) | 文件不存在 |
| 不变量被破坏 | 网络超时 |
go
// ✅ panic 的合理使用
func MustCompile(pattern string) *regexp.Regexp {
re, err := regexp.Compile(pattern)
if err != nil {
panic(err) // 编译时就应该发现的错误
}
return re
}
// ❌ panic 的滥用
func GetUser(id int) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // 不要把业务错误变成 panic!
}
return user
}
15.9.3 recover 是什么
go
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获到 panic:", r)
}
}()
panic("出错了!")
fmt.Println("这行不会执行")
}
func main() {
safeCall()
fmt.Println("程序继续运行") // ✅ 会执行
}
recover 只能在 defer 函数中生效,它截获 panic,让程序恢复运行。
15.10 defer 中的 recover
15.10.1 标准模式
go
// HTTP服务器中防止单个请求panic导致整个服务崩溃
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
15.10.2 在goroutine中recover
go
// ⚠️ 每个goroutine必须有自己的recover!
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("goroutine panic:", r)
}
}()
// 可能panic的代码
riskyOperation()
}()
// ❌ 外层recover无法捕获goroutine中的panic
go func() {
riskyOperation() // panic会导致整个程序崩溃!
}()
15.10.3 recover 的重要限制
go
// ❌ recover 不能在 defer 外面生效
func wrong() {
r := recover() // 永远返回 nil!
fmt.Println(r)
}
// ❌ recover 不能跨goroutine
// ❌ recover 不能捕获 os.Exit 导致的退出
// ❌ recover 不能捕获 fatal error(如栈溢出)
15.11 错误处理最佳实践
15.11.1 十大黄金法则
1. 永远不要忽略错误
go
// ❌ 忽略错误
json.Unmarshal(data, &v)
// ✅ 处理错误
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf("解析JSON失败: %w", err)
}
2. 错误只处理一次
go
// ❌ 打印后又返回------重复处理
if err != nil {
log.Println("错误:", err) // 处理了
return err // 又传递了
}
// ✅ 要么处理(吞掉),要么传递(返回),二选一
if err != nil {
return fmt.Errorf("操作失败: %w", err) // 添加上下文后传递
}
3. 添加上下文信息
go
// ❌ 仅返回底层错误
return err
// ✅ 添加有意义的上下文
return fmt.Errorf("用户 %d 查询订单失败: %w", userID, err)
4. 使用 %w 保持错误链
go
return fmt.Errorf("读取配置: %w", err) // ✅ 保留
return fmt.Errorf("读取配置: %v", err) // ❌ 断链
5. 错误信息小写开头、无标点
go
// ✅ Go惯例:小写开头,无句号
errors.New("用户未找到")
fmt.Errorf("id=%d: %w", id, err)
// ❌ 违反惯例
errors.New("User not found.")
errors.New("错误:用户未找到!")
15.11.2 更多法则
| # | 法则 | 说明 |
|---|---|---|
| 6 | 尽早返回错误 | 减少嵌套 if err == nil { ... } |
| 7 | 定义哨兵错误 | 可预期的固定错误用 var ErrXxx |
| 8 | 错误判断用 errors.Is/As |
不用 == 判断包装后的错误 |
| 9 | panic只用于不可恢复的错误 | 业务错误永远是 error 返回值 |
| 10 | 每个goroutine都要recover | 一个goroutine的panic不会自动被另一个捕获 |
15.11.3 早返回 vs 深层嵌套
go
// ❌ 深层嵌套
func process() error {
f, err := os.Open("file.txt")
if err == nil {
defer f.Close()
data, err := io.ReadAll(f)
if err == nil {
result, err := parse(data)
if err == nil {
return save(result)
} else {
return err
}
} else {
return err
}
} else {
return err
}
}
// ✅ 早返回(Happy Path左对齐)
func process() error {
f, err := os.Open("file.txt")
if err != nil {
return fmt.Errorf("打开文件失败: %w", err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("读取文件失败: %w", err)
}
result, err := parse(data)
if err != nil {
return fmt.Errorf("解析失败: %w", err)
}
return save(result)
}
15.12 综合案例:文件处理服务
go
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
)
// ── 哨兵错误 ──
var (
ErrFileNotFound = errors.New("文件未找到")
ErrInvalidJSON = errors.New("无效的JSON格式")
ErrEmptyContent = errors.New("内容为空")
)
// ── 自定义错误类型 ──
type FileError struct {
Op string // 操作:open/read/parse
Path string // 文件路径
Err error // 底层错误
}
func (e *FileError) Error() string {
return fmt.Sprintf("文件操作[%s] %s 失败: %v", e.Op, e.Path, e.Err)
}
func (e *FileError) Unwrap() error { return e.Err }
// ── 服务层 ──
type Config struct {
Port int `json:"port"`
Host string `json:"host"`
Debug bool `json:"debug"`
}
func LoadConfig(path string) (*Config, error) {
// 1. 打开文件
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %s", ErrFileNotFound, path)
}
return nil, &FileError{Op: "read", Path: path, Err: err}
}
// 2. 验证非空
if len(data) == 0 {
return nil, ErrEmptyContent
}
// 3. 解析JSON
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidJSON, err)
}
// 4. 业务校验
if config.Port <= 0 || config.Port > 65535 {
return nil, fmt.Errorf("端口号无效: %d", config.Port)
}
return &config, nil
}
// ── 调用方 ──
func main() {
config, err := LoadConfig("config.json")
if err != nil {
// 分类处理
switch {
case errors.Is(err, ErrFileNotFound):
fmt.Println("📁 配置文件不存在,使用默认配置")
// 使用默认配置
case errors.Is(err, ErrInvalidJSON):
fmt.Println("📝 配置文件格式错误:", err)
return
case errors.Is(err, ErrEmptyContent):
fmt.Println("📄 配置文件为空")
return
default:
// 尝试提取文件错误详情
var fileErr *FileError
if errors.As(err, &fileErr) {
fmt.Printf("🔴 文件操作异常 [%s]: %s\n", fileErr.Op, fileErr.Path)
}
fmt.Println("❌ 加载配置失败:", err)
return
}
}
fmt.Printf("✅ 配置加载成功: %+v\n", config)
}
15.13 本章总结
| 知识点 | 要点回顾 |
|---|---|
| Go错误哲学 | 错误是值,显式处理,不依赖异常 |
| error接口 | 仅 Error() string 一个方法 |
| 创建错误 | errors.New、fmt.Errorf(用 %w 包装) |
| 错误包装 | %w 保留错误链;%v 断链 |
| errors.Is | 沿链判断是否包含特定值(sentinel) |
| errors.As | 沿链提取特定类型的错误 |
| 自定义错误 | 实现 Error() + Unwrap() |
| Sentinel Error | 包级 var ErrXxx = errors.New(...) |
| panic/recover | 紧急机制,不是异常处理;defer中recover |
| 最佳实践 | 不忽略、不重复处理、添加上下文、早返回 |
💬 思考与互动 :你在项目中是如何组织错误处理的?用了自定义错误类型还是哨兵错误?遇到过
errors.Is和errors.As的混淆吗?欢迎评论区分享你的Go错误处理心法!
本文是「Go入门到精通 2026」系列第15篇。上一篇:14-Go 入门到精通-包与模块管理 | 下一篇:敬请期待...