mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 09:46:37 +08:00
wavelet init
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# 错误流程模式
|
||||
|
||||
错误流程、一次处理原则和日志决策的详细模式。
|
||||
|
||||
## 缩进错误流程
|
||||
|
||||
在继续正常代码之前先处理错误。这通过使读者能够快速找到正常路径来提高可读性。
|
||||
|
||||
```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 | 开发期间有用的诊断细节 |
|
||||
@@ -0,0 +1,151 @@
|
||||
# 错误类型参考
|
||||
|
||||
本参考涵盖结构化错误类型、哨兵错误,以及如何为你的用例选择正确的错误类型。
|
||||
|
||||
---
|
||||
|
||||
## 错误结构
|
||||
|
||||
> 错误类型决策表在父技能中(SKILL.md § 错误类型)。
|
||||
> 本参考涵盖:扩展的代码示例、哨兵错误、使用 `errors.Is`/`errors.As` 进行错误检查,以及结构化错误类型。
|
||||
|
||||
**关键考虑因素**:
|
||||
|
||||
- 调用者是否需要使用 `errors.Is` 或 `errors.As` 来匹配错误?
|
||||
- 错误消息是静态的还是需要运行时值?
|
||||
- 导出的错误变量/类型将成为公共 API 的一部分
|
||||
|
||||
```go
|
||||
// 无需匹配,静态消息
|
||||
func Open() error {
|
||||
return errors.New("could not open")
|
||||
}
|
||||
|
||||
// 需要匹配,静态消息 - 导出哨兵
|
||||
var ErrCouldNotOpen = errors.New("could not open")
|
||||
|
||||
func Open() error {
|
||||
return ErrCouldNotOpen
|
||||
}
|
||||
|
||||
// 需要匹配,动态消息 - 使用自定义类型
|
||||
type NotFoundError struct {
|
||||
File string
|
||||
}
|
||||
|
||||
func (e *NotFoundError) Error() string {
|
||||
return fmt.Sprintf("file %q not found", e.File)
|
||||
}
|
||||
|
||||
func Open(file string) error {
|
||||
return &NotFoundError{File: file}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 哨兵错误
|
||||
|
||||
最简单的结构化错误是无参数化的全局值:
|
||||
|
||||
```go
|
||||
// 好:用于程序化检查的哨兵错误
|
||||
var (
|
||||
// ErrDuplicate 在该动物已被见过时发生。
|
||||
ErrDuplicate = errors.New("duplicate")
|
||||
|
||||
// ErrMarsupial 因为我们不支持有袋类动物。
|
||||
ErrMarsupial = errors.New("marsupials are not supported")
|
||||
)
|
||||
|
||||
func process(animal Animal) error {
|
||||
switch {
|
||||
case seen[animal]:
|
||||
return ErrDuplicate
|
||||
case marsupial(animal):
|
||||
return ErrMarsupial
|
||||
}
|
||||
seen[animal] = true
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 检查错误
|
||||
|
||||
对于直接比较(当错误未被包装时):
|
||||
|
||||
```go
|
||||
// 好:与哨兵直接比较
|
||||
switch err := process(an); err {
|
||||
case ErrDuplicate:
|
||||
return fmt.Errorf("feed %q: %v", an, err)
|
||||
case ErrMarsupial:
|
||||
alternate := an.BackupAnimal()
|
||||
return handlePet(alternate)
|
||||
}
|
||||
```
|
||||
|
||||
当错误可能被包装时,使用 `errors.Is`:
|
||||
|
||||
```go
|
||||
// 好:适用于被包装的错误
|
||||
switch err := process(an); {
|
||||
case errors.Is(err, ErrDuplicate):
|
||||
return fmt.Errorf("feed %q: %v", an, err)
|
||||
case errors.Is(err, ErrMarsupial):
|
||||
// 尝试恢复...
|
||||
}
|
||||
```
|
||||
|
||||
**绝不**基于字符串内容匹配错误:
|
||||
|
||||
```go
|
||||
// 不好:脆弱的字符串匹配
|
||||
if regexp.MatchString(`duplicate`, err.Error()) {...}
|
||||
if regexp.MatchString(`marsupial`, err.Error()) {...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 结构化错误类型
|
||||
|
||||
对于需要额外程序化信息的错误,使用结构体类型:
|
||||
|
||||
```go
|
||||
// 好:具有可访问字段的结构化错误
|
||||
type PathError struct {
|
||||
Op string
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *PathError) Error() string {
|
||||
return e.Op + " " + e.Path + ": " + e.Err.Error()
|
||||
}
|
||||
|
||||
func (e *PathError) Unwrap() error { return e.Err }
|
||||
```
|
||||
|
||||
调用者可以使用 `errors.As` 提取结构化错误:
|
||||
|
||||
```go
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
fmt.Println("Failed path:", pathErr.Path)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 场景 | 错误类型 |
|
||||
|------|---------|
|
||||
| 无需匹配,静态消息 | `errors.New("message")` |
|
||||
| 无需匹配,动态消息 | `fmt.Errorf("msg: %v", val)` |
|
||||
| 需要匹配,静态消息 | `var ErrFoo = errors.New(...)` |
|
||||
| 需要匹配,动态消息 | 自定义结构体类型 |
|
||||
| 检查哨兵错误 | `errors.Is(err, ErrFoo)` |
|
||||
| 提取结构化错误 | `errors.As(err, &target)` |
|
||||
@@ -0,0 +1,174 @@
|
||||
# 错误包装参考
|
||||
|
||||
本参考涵盖使用 `%v` vs `%w` 的错误包装、放置约定、向错误添加上下文以及日志最佳实践。
|
||||
|
||||
---
|
||||
|
||||
## 包装错误:%v vs %w
|
||||
|
||||
> **建议**:推荐的最佳实践。
|
||||
|
||||
`%v` 和 `%w` 的选择会显著影响错误的传播和检查方式。
|
||||
|
||||
### 使用 %v 进行简单注释
|
||||
|
||||
当你需要以下操作时使用 `%v`:
|
||||
|
||||
- 添加上下文但不保留错误链以供程序化检查
|
||||
- 创建全新的、独立的错误(特别是在 RPC/IPC 等系统边界)
|
||||
- 向人类记录或显示错误
|
||||
|
||||
```go
|
||||
// 好:%v 在系统边界 — 隐藏内部细节
|
||||
func (s *Server) SuggestFortune(ctx context.Context, req *pb.Request) (*pb.Response, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't find fortune database: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 %w 保留错误链
|
||||
|
||||
当你需要调用者以编程方式检查底层错误时使用 `%w`:
|
||||
|
||||
```go
|
||||
// 好:%w 保留错误链以供 errors.Is/errors.As 使用
|
||||
func (s *Server) internalFunction(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't find remote file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用者现在可以检查:
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// 处理未找到的情况
|
||||
}
|
||||
```
|
||||
|
||||
### 何时使用哪种
|
||||
|
||||
**使用 %w 的场景**:
|
||||
- 在添加上下文的同时保留原始错误以供程序化检查
|
||||
- 你明确记录并测试了所暴露的底层错误
|
||||
|
||||
**使用 %v 的场景**:
|
||||
- 在系统边界(RPC、IPC、存储)转换为规范错误空间
|
||||
- 向人类记录日志或显示
|
||||
- 创建隐藏实现细节的独立错误
|
||||
|
||||
---
|
||||
|
||||
## %w 的放置位置
|
||||
|
||||
> **建议**:推荐的最佳实践。
|
||||
|
||||
将 `%w` 放在错误字符串的**末尾**,使错误文本反映错误链结构:
|
||||
|
||||
```go
|
||||
// 好:%w 在末尾 — 从最新到最旧打印
|
||||
err1 := fmt.Errorf("err1")
|
||||
err2 := fmt.Errorf("err2: %w", err1)
|
||||
err3 := fmt.Errorf("err3: %w", err2)
|
||||
fmt.Println(err3) // err3: err2: err1
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:%w 在开头 — 从最旧到最新打印(令人困惑)
|
||||
err1 := fmt.Errorf("err1")
|
||||
err2 := fmt.Errorf("%w: err2", err1)
|
||||
err3 := fmt.Errorf("%w: err3", err2)
|
||||
fmt.Println(err3) // err1: err2: err3
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:%w 在中间 — 不连贯的顺序
|
||||
err1 := fmt.Errorf("err1")
|
||||
err2 := fmt.Errorf("err2-1 %w err2-2", err1)
|
||||
err3 := fmt.Errorf("err3-1 %w err3-2", err2)
|
||||
fmt.Println(err3) // err3-1 err2-1 err1 err2-2 err3-2
|
||||
```
|
||||
|
||||
**模式**:使用 `context message: %w` 的形式
|
||||
|
||||
---
|
||||
|
||||
## 向错误添加信息
|
||||
|
||||
> **建议**:推荐的最佳实践。
|
||||
|
||||
### 添加上下文,而非冗余
|
||||
|
||||
添加你拥有但调用者/被调用者可能没有的信息。避免重复底层错误已提供的信息:
|
||||
|
||||
```go
|
||||
// 好:添加有意义的上下文
|
||||
if err := os.Open("settings.txt"); err != nil {
|
||||
return fmt.Errorf("launch codes unavailable: %v", err)
|
||||
}
|
||||
// 输出:launch codes unavailable: open settings.txt: no such file or directory
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:重复了文件名
|
||||
if err := os.Open("settings.txt"); err != nil {
|
||||
return fmt.Errorf("could not open settings.txt: %v", err)
|
||||
}
|
||||
// 输出:could not open settings.txt: open settings.txt: no such file or directory
|
||||
```
|
||||
|
||||
### 不要无目的地注释
|
||||
|
||||
如果注释仅表示失败而没有添加信息,直接返回错误:
|
||||
|
||||
```go
|
||||
// 不好:注释没有增加信息
|
||||
return fmt.Errorf("failed: %v", err)
|
||||
|
||||
// 好:直接返回错误
|
||||
return err
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 记录错误日志
|
||||
|
||||
> **建议**:推荐的最佳实践。
|
||||
|
||||
当需要记录错误时,使用 `log/slog`(Go 1.21+)配合结构化键值对和适当的日志级别:
|
||||
|
||||
- **`slog.Error`**:保留用于需要调查的可操作问题。
|
||||
- **`slog.Warn`**:用于可能需要关注但不可立即操作的问题。
|
||||
- **`slog.Debug`**:用于开发追踪 — 仅在 handler 级别设为 `LevelDebug` 时才输出。
|
||||
|
||||
```go
|
||||
// 好:使用适当级别的结构化日志
|
||||
for _, q := range queries {
|
||||
slog.Debug("handling query", "query", q)
|
||||
q.Run()
|
||||
}
|
||||
|
||||
// 好:在级别检查后保护昂贵的格式化操作
|
||||
if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
|
||||
slog.Debug("query plan", "explain", q.Explain())
|
||||
}
|
||||
|
||||
// 不好:即使禁用了 debug 日志也会执行昂贵的调用
|
||||
slog.Debug("query plan", "explain", q.Explain())
|
||||
```
|
||||
|
||||
### 保护敏感信息
|
||||
|
||||
注意日志消息中的 PII(个人身份信息)。许多日志接收器不适合存放敏感用户数据。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 指导 |
|
||||
|------|------|
|
||||
| `%v` | 在系统边界使用、用于日志记录、隐藏细节 |
|
||||
| `%w` | 保留错误链以供程序化检查 |
|
||||
| `%w` 放置 | 始终在末尾:`"context: %w"` |
|
||||
| 添加上下文 | 添加新信息,不要重复现有信息 |
|
||||
| 空注释 | 直接返回 `err` 而非 `fmt.Errorf("failed: %v", err)` |
|
||||
| 日志 | 不要既记录日志又返回;使用适当的日志级别 |
|
||||
Reference in New Issue
Block a user