Files
OpenFlare/.agents/skills/go-error-handling/references/ERROR-FLOW.md
T
2026-08-04 11:40:32 +08:00

154 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 错误流程模式
错误流程、一次处理原则和日志决策的详细模式。
## 缩进错误流程
在继续正常代码之前先处理错误。这通过使读者能够快速找到正常路径来提高可读性。
```go
// 好:错误处理优先,正常代码无缩进
if err != nil {
// 错误处理
return // 或 continue 等
}
// 正常代码
```
```go
// 不好:正常代码隐藏在 else 子句中
if err != nil {
// 错误处理
} else {
// 正常代码因缩进看起来不自然
}
```
### 避免对长期使用的变量使用 if 初始化语句
如果变量在多行中使用,将声明移出:
```go
// 好:声明与错误检查分开
x, err := f()
if err != nil {
return err
}
// 大量使用 x 的代码
// 跨越多行
```
```go
// 不好:变量作用域限制在 else 块中,难以阅读
if x, err := f(); err != nil {
return err
} else {
// 大量使用 x 的代码
// 跨越多行
}
```
---
## 错误只处理一次
当调用者收到错误时,应该**只处理一次**。选择一种响应方式:
1. **返回错误**(包装或原文)让调用者处理
2. **记录日志并优雅降级**(不返回错误)
3. **匹配并处理**特定错误情况,返回其他错误
**如果返回了错误,就不要自己记录日志** — 让调用者处理。对同一错误既记录日志又返回是最常见的"一次处理"违规,导致重复噪音,因为调用栈上层的调用者也会处理该错误。
```go
// 不好:既记录日志又返回 — 导致日志噪音
u, err := getUser(id)
if err != nil {
log.Printf("Could not get user %q: %v", id, err)
return err // 调用者也会记录这个!
}
// 好:包装并返回 — 让调用者决定如何处理
u, err := getUser(id)
if err != nil {
return fmt.Errorf("get user %q: %w", id, err)
}
// 好:记录日志并优雅降级(不返回错误)
if err := emitMetrics(); err != nil {
// 写入指标失败不应影响应用程序
log.Printf("Could not emit metrics: %v", err)
}
// 继续执行...
// 好:匹配特定错误,返回其他错误
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// 用户不存在,使用 UTC
tz = time.UTC
} else {
return fmt.Errorf("get user %q: %w", id, err)
}
}
```
---
## 记录日志 vs 返回错误
> 错误只处理一次 — 记录日志或返回,不要两者都做。
### 决策流程
```
遇到错误?
├─ 调用者可以采取行动?→ 返回错误(通过 %w 附带上下文)
├─ 在调用链顶部?→ 记录日志并处理(返回 HTTP 状态码、退出等)
└─ 都不是?→ 以适当级别记录日志并继续
```
### 不要既记录日志又返回
```go
// 不好:错误既被记录又被返回 — 在日志中出现两次
func process(ctx context.Context, id string) error {
result, err := fetch(ctx, id)
if err != nil {
log.Printf("failed to fetch %s: %v", id, err)
return fmt.Errorf("fetching %s: %w", id, err)
}
return handle(result)
}
// 好:带上下文返回 — 让调用者决定是否记录日志
func process(ctx context.Context, id string) error {
result, err := fetch(ctx, id)
if err != nil {
return fmt.Errorf("fetching %s: %w", id, err)
}
return handle(result)
}
```
### 结构化日志
在生产代码中,优先使用结构化日志(Go 1.21+ 的 `slog`,或 `log/slog` 兼容库)而非 `log.Printf`:
```go
// 好:结构化字段可被机器解析
slog.Error("fetch failed", "id", id, "err", err)
// 避免:非结构化的字符串插值
log.Printf("fetch failed for %s: %v", id, err)
```
### 日志级别
| 级别 | 使用场景 |
|------|---------|
| Error | 需要关注的可操作故障 |
| Warn | 不需要立即处理的降级行为 |
| Info | 关键生命周期事件(启动、关闭、配置加载) |
| Debug | 开发期间有用的诊断细节 |