第 7 章 · 错误处理与 defer
本章目标:掌握 error 接口、fmt.Errorf 与 %w 包装、errors.Is / errors.As;熟练使用 defer 关闭资源;理解 panic/recover 仅用于不可恢复错误;在 toolkit-go 中安全读写日志文件;对照 java-dev ch07 异常机制与 python-dev try/except。
学时建议:3~4 小时(含 1.5 小时跟练)
前置:完成 go-dev ch06;建议已学 java-dev ch07 或 python-dev 异常章节。
7.1 场景说明:读取 access 日志
toolkit-go(~/learn-go/toolkit-go,模块 example.com/toolkit-go)从磁盘读取 JSON Lines 或管道分隔日志。文件可能不存在、权限不足或行格式损坏——Go 用 error 返回值 逐层向上传递,而非 try/catch。
internal/logparser/
├── parser.go
├── file_reader.go
└── errors.go # 哨兵错误
| Java | Go | Python |
|---|---|---|
try/catch/finally | if err != nil { return err } + defer | |
| 受检/非受检异常 | 统一 error 接口 | |
throw new XxxException | return fmt.Errorf(...) | |
finally { close } | defer f.Close() |
7.2 error 接口与创建错误
// error 是内置接口
type error interface {
Error() string
}
// internal/logparser/errors.go
package logparser
import "errors"
var (
ErrEmptyLog = errors.New("empty log file")
ErrBadLine = errors.New("invalid log line")
)
// internal/logparser/file_reader.go
package logparser
import (
"fmt"
"os"
)
func OpenLog(path string) (*os.File, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open %s: %w", path, err)
}
return f, nil
}
| 创建方式 | 示例 | 用途 |
|---|---|---|
errors.New | 哨兵错误 | errors.Is 判断 |
fmt.Errorf | 格式化消息 | 添加上下文 |
fmt.Errorf("...: %w", err) | 包装 | 保留错误链 |
| 自定义类型 | type MyErr struct{} | errors.As 提取 |
7.3 错误处理惯用法
func Process(path string) error {
f, err := OpenLog(path)
if err != nil {
return err // 或再包装一层
}
defer f.Close()
data, err := readAll(f)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
if len(data) == 0 {
return ErrEmptyLog
}
return nil
}
不要:
// 反模式:panic 处理普通 IO 失败
if err != nil {
panic(err)
}
7.4 defer 释放资源
defer 将函数调用推迟到当前函数返回前执行,LIFO(后进先出)顺序。
func ReadAllLines(path string) (lines []string, err error) {
f, err := OpenLog(path)
if err != nil {
return nil, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scan: %w", err)
}
if len(lines) == 0 {
return nil, ErrEmptyLog
}
return lines, nil
}
| defer 用途 | 示例 |
|---|---|
| 关闭文件 | defer f.Close() |
| 解锁 | defer mu.Unlock() |
| 记录耗时 | defer func(t time.Time) { log.Printf("took %v", time.Since(t)) }(time.Now()) |
| 恢复 panic | defer func() { recover() }() |
defer 执行时机:在 return 之后、函数真正退出之前;多个 defer 按声明逆序执行。
7.5 errors.Is 与 errors.As
import (
"errors"
"fmt"
"os"
)
func HandleOpen(path string) {
_, err := OpenLog(path)
if err == nil {
return
}
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在,请检查路径")
return
}
if errors.Is(err, ErrEmptyLog) {
fmt.Println("日志为空")
return
}
fmt.Println("其他错误:", err)
}
// errors.As 提取具体类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("路径:", pathErr.Path)
}
| 函数 | 用途 |
|---|---|
errors.Is(err, target) | 判断错误链中是否包含 target |
errors.As(err, &ptr) | 提取链中某具体类型 |
%w | fmt.Errorf 包装,保留链 |
errors.Unwrap | 手动 unwrap(少用) |
7.6 完整示例:ParseFile
// internal/logparser/parse_file.go
package logparser
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
type AccessLog struct {
Path string
DurationMs int
Status int
}
func parseLine(line string) (AccessLog, error) {
parts := strings.Split(strings.TrimSpace(line), "|")
if len(parts) != 3 {
return AccessLog{}, fmt.Errorf("%w: %q", ErrBadLine, line)
}
ms, err := strconv.Atoi(parts[1])
if err != nil {
return AccessLog{}, fmt.Errorf("%w: bad duration", ErrBadLine)
}
status, err := strconv.Atoi(parts[2])
if err != nil {
return AccessLog{}, fmt.Errorf("%w: bad status", ErrBadLine)
}
return AccessLog{Path: parts[0], DurationMs: ms, Status: status}, nil
}
func ParseFile(path string) ([]AccessLog, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("parse file: %w", err)
}
defer f.Close()
var logs []AccessLog
scanner := bufio.NewScanner(f)
lineNo := 0
for scanner.Scan() {
lineNo++
entry, err := parseLine(scanner.Text())
if err != nil {
return nil, fmt.Errorf("line %d: %w", lineNo, err)