外观
Go Error Handling
Phase 02 — Go Core 涵盖:
error·defer·panic·recover
1. 学习目标
完成本知识点后,你应该能够:
- 理解 Go 中 error 是返回值而非异常
- 使用
errors.New、fmt.Errorf创建和包装错误 - 掌握
if err != nil的错误处理惯用法 - 正确使用
defer释放资源(文件、连接、锁) - 理解
panic/recover的适用场景与限制 - 在 API 和数字孪生数据处理中编写健壮的错误处理代码
2. 为什么需要
后端服务必须假设任何 I/O 都可能失败:数据库断连、JSON 格式错误、设备离线、磁盘满。Go 没有 try-catch,而是把错误作为函数返回值显式传递——调用者不能忽略失败路径(编译器会提示未使用的 err)。
defer 保证函数退出前执行清理逻辑,避免忘记 Close() 导致连接泄漏。panic 用于不可恢复的编程错误,正常业务逻辑(如参数校验失败)应返回 error,而非 panic。
3. 核心概念
3.1 error 接口
go
type error interface {
Error() string
}任何实现了 Error() string 的类型都是 error。最常用的是 errors.New 和 fmt.Errorf。
3.2 错误处理惯用法
go
result, err := doSomething()
if err != nil {
return nil, err // 向上传递
}
// 正常使用 result3.3 错误包装(Go 1.13+)
go
return fmt.Errorf("load device %s: %w", id, err)%w 包装底层错误,可用 errors.Is / errors.As 判断。
3.4 defer
函数 return 前逆序执行 defer 语句。常用于 file.Close()、mu.Unlock()。
3.5 panic 与 recover
panic(v):停止正常执行,展开栈,若无 recover 则程序崩溃recover():仅在 defer 函数内有效,捕获 panic 值
原则:业务错误用 error;真正不可恢复或编程 bug 才 panic。
4. 基础语法
完整示例见 workspace/phase-02/error-handling/main.go。
go
package main
import (
"errors"
"fmt"
"os"
)
var ErrDeviceOffline = errors.New("device offline")
func fetchTelemetry(deviceID string) (float64, error) {
if deviceID == "" {
return 0, errors.New("deviceID is required")
}
if deviceID == "offline-01" {
return 0, fmt.Errorf("fetch %s: %w", deviceID, ErrDeviceOffline)
}
return 36.5, nil
}
func readConfig(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("open config: %w", err)
}
defer f.Close() // 函数返回前关闭文件
buf := make([]byte, 64)
n, err := f.Read(buf)
if err != nil {
return "", fmt.Errorf("read config: %w", err)
}
return string(buf[:n]), nil
}
func safeDivide(a, b float64) (result float64, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered panic: %v", r)
}
}()
if b == 0 {
panic("division by zero")
}
return a / b, nil
}
func main() {
temp, err := fetchTelemetry("sensor-01")
if err != nil {
fmt.Println("error:", err)
if errors.Is(err, ErrDeviceOffline) {
fmt.Println("→ device is offline, retry later")
}
return
}
fmt.Println("temperature:", temp)
_, err = safeDivide(10, 0)
if err != nil {
fmt.Println(err)
}
}5. 代码解析
go
if err != nil { return nil, err }Go 最常见的模式。先处理错误,再处理正常逻辑——与 JS 的 happy path 嵌套相反。
go
defer f.Close()注册清理函数。即使中间 return 或 panic,defer 仍会执行(panic 时先执行 defer 再崩溃)。
go
fmt.Errorf("fetch %s: %w", deviceID, ErrDeviceOffline)%w 保留错误链,errors.Is(err, ErrDeviceOffline) 可穿透包装层判断。
go
defer func() { if r := recover(); r != nil { ... } }()在 defer 中 recover,将 panic 转为 error 返回——HTTP handler 中偶尔使用,但业务层优先返回 error。
6. JavaScript / TypeScript 对比
| 概念 | Go | JavaScript / TypeScript |
|---|---|---|
| 错误传递 | 返回值 error | throw / try-catch |
| 强制处理 | 惯用法要求检查 err | catch 可省略 |
| 错误类型 | 实现 error interface | Error 类及子类 |
| 资源清理 | defer | try-finally / using |
| 不可恢复 | panic | uncaught exception |
| 捕获 | recover() 仅 defer 内 | catch (e) 任意层 |
关键差异:
- Go error 是值,不是控制流异常;忽略 err 不会崩溃,但会产生 bug
defer在 return 表达式求值之后、真正返回之前执行- 不要用 panic 代替 validation error(如
"invalid JSON"应返回 error)
7. 常见错误
错误 1:忽略 error
go
data, _ := json.Marshal(v) // ❌ 吞掉错误错误 2:defer 闭包捕获循环变量
go
for _, f := range files {
defer f.Close() // ❌ 全部 defer 同一个 f
}
// ✅ 用 func() { f.Close() }() 或 defer 在循环外处理错误 3:在业务逻辑中 panic
go
if req.ID == "" {
panic("bad request") // ❌ 应 return error 或 HTTP 400
}错误 4:recover 不在 defer 中
go
if r := recover(); r != nil { ... } // ❌ 无效,recover 只在 defer 内工作错误 5:丢失错误上下文
go
return err // 可以,但缺少操作上下文
return fmt.Errorf("save scene %s: %w", id, err) // ✅ 更好8. 实际应用
数字孪生 — 设备数据拉取
go
func PullDeviceState(id string) (*DeviceState, error) {
raw, err := fetchFromPLC(id)
if err != nil {
return nil, fmt.Errorf("plc fetch %s: %w", id, err)
}
state, err := parseState(raw)
if err != nil {
return nil, fmt.Errorf("parse state %s: %w", id, err)
}
return state, nil
}每一层添加上下文,日志和 API 响应能定位到具体环节。
Web API — defer 关闭 Body
go
resp, err := http.Get(url)
if err != nil { return err }
defer resp.Body.Close()不 defer 会导致连接池耗尽——生产环境常见泄漏点。
9. 深入理解
9.1 defer 执行顺序
多个 defer 按**后进先出(LIFO)**执行。defer 的参数在注册时求值。
9.2 errors.Is vs errors.As
errors.Is(err, target):判断错误链中是否包含某 sentinel errorerrors.As(err, &target):提取链中某具体错误类型
9.3 panic 的成本
panic 会触发栈展开,比 return error 慢得多。HTTP 框架可能在 middleware 中 recover panic 并返回 500,但不应依赖此机制处理正常错误。
9.4 何时 panic
- 初始化失败(如 config 缺失且无法启动)
- 不可能的分支(编程 invariant 被破坏)
- 标准库
Must函数(如template.Must)——仅用于 init 阶段
10. 练习
详细题目见
exercises/phase-02-core/02-error-handling.md。
Level 1 — 基础
练习 1.1:编写 parseSpeed(s string) (float64, error),空字符串和非法数字返回 error。
练习 1.2:用 errors.New 定义 sentinel error ErrNotFound。
练习 1.3:用 defer 打印函数进入和退出日志。
Level 2 — 应用
练习 2.1:用 fmt.Errorf 和 %w 包装错误,配合 errors.Is 判断。
练习 2.2:模拟文件读写,defer Close,处理 Open 失败。
练习 2.3:在 defer+recover 中将 panic 转为 error 返回。
Level 3 — 综合
练习 3.1:实现设备数据管道:fetch → parse → validate,任一步失败带上下文返回。
练习 3.2:设计错误类型 ValidationError,用 errors.As 提取。
Level 4 — 项目实践
练习 4.1:在 workspace/phase-02/error-handling/ 构建 mini telemetry 服务:多步骤处理、defer 清理、统一 error 包装。
11. 学习检查
- Go 为什么用 error 返回值而不是 try-catch?
defer的执行时机和顺序是什么?%w和%v在fmt.Errorf中有何区别?- 什么场景适合 panic,什么场景必须返回 error?
recover()为什么必须在 defer 函数内调用?errors.Is和直接err == target有何不同?
12. 下一步
| 已完成 | 下一知识点 | 关系 |
|---|---|---|
| error, defer, panic, recover | JSON | 解析 JSON 时大量 error 处理 |
| goroutine | 并发中 error 需通过 channel 或 sync 传递 |
建议先掌握 JSON 序列化,再进入并发——并发中的 error 传递是进阶话题。