Go 基础语法 - 错误处理全解
2025/5/22大约 4 分钟
当然可以!以下是专为你定制的实践指南,完全基于你的开发环境:
- 操作系统:Linux Mint XFCE
- Go 版本:go1.22.2 linux/amd64
- 项目目录:
/home/liumangmang/GolandProjects
📌 标题:
Go 错误处理全解:error、defer、panic/recover 与错误封装实战
✅ 步骤 1:创建新项目
打开终端,执行:
cd /home/liumangmang/GolandProjects
mkdir go-error-handling && cd go-error-handling
go mod init go-error-handling✅ 步骤 2:编写演示代码(main.go)
创建并编辑 main.go:
nano main.go粘贴以下完整示例代码(涵盖 error 返回、defer 清理、panic 恢复、fmt.Errorf 封装):
package main
import (
"errors"
"fmt"
"os"
)
// ========== 1. 自定义错误 + fmt.Errorf 封装 ==========
func divide(a, b float64) (float64, error) {
if b == 0 {
// 使用 fmt.Errorf 包装原始错误,添加上下文
return 0, fmt.Errorf("divide by zero: cannot divide %.2f by %.2f", a, b)
}
return a / b, nil
}
// 更复杂的错误链(Go 1.13+ 支持 %w)
var ErrNegativeInput = errors.New("input must be non-negative")
func sqrt(x float64) (float64, error) {
if x < 0 {
// 使用 %w 包装错误,支持 errors.Is 和 errors.As
return 0, fmt.Errorf("invalid input for sqrt: %w", ErrNegativeInput)
}
return x * x, nil // 注意:这里故意写成平方,方便测试
}
// ========== 2. defer 的典型用途 ==========
func readFile(filename string) error {
fmt.Println("尝试打开文件:", filename)
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("无法打开文件 %s: %w", filename, err)
}
// defer 确保文件在函数退出前关闭(即使 panic)
defer func() {
fmt.Println("defer: 关闭文件")
file.Close()
}()
// 模拟读取
fmt.Println("文件已打开,正在读取...")
return nil
}
// ========== 3. panic 与 recover ==========
func riskyFunction(n int) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("recover 捕获到 panic: %v\n", r)
}
}()
if n < 0 {
panic("n 不能为负数!")
}
fmt.Println("riskyFunction 正常执行,n =", n)
}
// recover 只在 defer 中有效!
func noRecover() {
// ❌ 这样写无法捕获 panic
recover()
panic("不会被捕获")
}
// ========== 4. 综合示例:安全计算 ==========
func safeCompute(a, b float64) {
defer fmt.Println("safeCompute 结束\n---")
fmt.Printf("计算 %.2f / %.2f\n", a, b)
result, err := divide(a, b)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Printf("结果: %.2f\n", result)
fmt.Println("尝试对结果开方(实际是平方)")
sq, err := sqrt(result)
if err != nil {
if errors.Is(err, ErrNegativeInput) {
fmt.Println("检测到特定错误:输入为负")
}
fmt.Println("sqrt 错误:", err)
return
}
fmt.Printf("平方结果: %.2f\n", sq)
}
// ========== 主函数 ==========
func main() {
fmt.Println("=== 1. 基本 error 处理 ===")
safeCompute(10, 2)
safeCompute(10, 0) // 触发除零错误
fmt.Println("\n=== 2. defer 文件操作 ===")
readFile("/nonexistent/file.txt") // 文件不存在,触发错误
readFile("/etc/hostname") // 存在的文件(Linux 系统通常有)
fmt.Println("\n=== 3. panic 与 recover ===")
riskyFunction(5)
riskyFunction(-1) // 触发 panic,但被 recover 捕获
fmt.Println("\n=== 4. 错误封装与识别 ===")
_, err := sqrt(-4)
if err != nil {
fmt.Println("原始错误信息:", err)
// 使用 errors.Is 判断是否包含特定错误
if errors.Is(err, ErrNegativeInput) {
fmt.Println("✅ 成功识别自定义错误 ErrNegativeInput")
}
}
// ⚠️ 取消注释下面这行会 crash(recover 不在 defer 中无效)
// noRecover()
}保存并退出(Ctrl+O → Enter → Ctrl+X)。
✅ 步骤 3:格式化并运行
go fmt
go run .预期输出(节选):
=== 1. 基本 error 处理 ===
计算 10.00 / 2.00
结果: 5.00
尝试对结果开方(实际是平方)
平方结果: 25.00
safeCompute 结束
---
计算 10.00 / 0.00
错误: divide by zero: cannot divide 10.00 by 0.00
safeCompute 结束
---
=== 2. defer 文件操作 ===
尝试打开文件: /nonexistent/file.txt
defer: 关闭文件
尝试打开文件: /etc/hostname
文件已打开,正在读取...
defer: 关闭文件
=== 3. panic 与 recover ===
riskyFunction 正常执行,n = 5
recover 捕获到 panic: n 不能为负数!
safeCompute 结束
---
=== 4. 错误封装与识别 ===
原始错误信息: invalid input for sqrt: input must be non-negative
✅ 成功识别自定义错误 ErrNegativeInput🔍 核心知识点总结
| 机制 | 说明 | 最佳实践 |
|---|---|---|
error | Go 的错误是值,不是异常 | 函数最后一个返回值通常是 error,必须显式检查 |
fmt.Errorf | 创建带格式的错误;用 %w 包装(Go 1.13+) | 用于添加上下文,支持 errors.Is/errors.As |
defer | 延迟执行,常用于资源清理(文件、锁、连接) | 确保 cleanup 逻辑总被执行,即使 panic |
panic | 立即停止当前 goroutine,开始栈展开 | 仅用于不可恢复的严重错误(如程序状态损坏) |
recover | 在 defer 中调用,可捕获 panic | 用于守护关键服务不崩溃(如 HTTP 服务器中间件) |
💡 重要原则:
- 不要滥用 panic:Go 推崇“显式错误处理”,而非异常。
- 永远检查 error:忽略 error 是 Go 新手常见错误。
- 用
%w而不是%v包装错误:这样才能用errors.Is判断。
✅ 在 GoLand 中调试建议
- 打开项目:
/home/liumangmang/GolandProjects/go-error-handling - 在
divide函数中设置断点,观察错误如何逐层返回 - 尝试取消注释
noRecover(),运行看程序崩溃(理解 recover 必须在 defer 中) - 使用 Evaluate Expression 功能测试
errors.Is(err, ErrNegativeInput)
🧭 下一步学习方向
- 自定义 error 类型(实现
Error() string方法) - 使用
errors.Unwrap()手动解包错误链 - 在 Web 服务中统一错误处理中间件
如果你希望我继续讲解 泛型入门、context 使用、并发模式(worker pool) 或 Go 单元测试(testing 包),欢迎随时告诉我!祝你写出健壮又优雅的 Go 代码 🚀
