Skip to content

Go Error Handling

Phase 02 — Go Core 涵盖:error · defer · panic · recover


1. 学习目标

完成本知识点后,你应该能够:

  • 理解 Go 中 error 是返回值而非异常
  • 使用 errors.Newfmt.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.Newfmt.Errorf

3.2 错误处理惯用法

go
result, err := doSomething()
if err != nil {
    return nil, err  // 向上传递
}
// 正常使用 result

3.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 对比

概念GoJavaScript / TypeScript
错误传递返回值 errorthrow / try-catch
强制处理惯用法要求检查 errcatch 可省略
错误类型实现 error interfaceError 类及子类
资源清理defertry-finally / using
不可恢复panicuncaught exception
捕获recover() 仅 defer 内catch (e) 任意层

关键差异

  1. Go error 是,不是控制流异常;忽略 err 不会崩溃,但会产生 bug
  2. defer 在 return 表达式求值之后、真正返回之前执行
  3. 不要用 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 error
  • errors.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. 学习检查

  1. Go 为什么用 error 返回值而不是 try-catch?
  2. defer 的执行时机和顺序是什么?
  3. %w%vfmt.Errorf 中有何区别?
  4. 什么场景适合 panic,什么场景必须返回 error?
  5. recover() 为什么必须在 defer 函数内调用?
  6. errors.Is 和直接 err == target 有何不同?

12. 下一步

已完成下一知识点关系
error, defer, panic, recoverJSON解析 JSON 时大量 error 处理
goroutine并发中 error 需通过 channel 或 sync 传递

建议先掌握 JSON 序列化,再进入并发——并发中的 error 传递是进阶话题。


学习导航

上一篇:Go Interface · 对应练习 · 下一篇:Go JSON