mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
154 lines
3.9 KiB
Markdown
154 lines
3.9 KiB
Markdown
# 错误流程模式
|
||
|
||
错误流程、一次处理原则和日志决策的详细模式。
|
||
|
||
## 缩进错误流程
|
||
|
||
在继续正常代码之前先处理错误。这通过使读者能够快速找到正常路径来提高可读性。
|
||
|
||
```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 | 开发期间有用的诊断细节 |
|