wavelet init

This commit is contained in:
ryan
2026-06-18 15:24:48 +08:00
parent d6a7011885
commit 99738bbc17
714 changed files with 139987 additions and 0 deletions
@@ -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)` |
| 日志 | 不要既记录日志又返回;使用适当的日志级别 |