mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
169 lines
6.1 KiB
Markdown
169 lines
6.1 KiB
Markdown
---
|
||
name: go-error-handling
|
||
description: Use when writing Go code that returns, wraps, or handles errors — choosing between sentinel errors, custom types, and fmt.Errorf (%w vs %v), structuring error flow, or deciding whether to log or return. Also use when propagating errors across package boundaries or using errors.Is/As, even if the user doesn't ask about error strategy. Does not cover panic/recover patterns (see go-defensive).
|
||
license: Apache-2.0
|
||
compatibility: Requires Go 1.13+ for errors.Is/errors.As and fmt.Errorf %w wrapping. Structured logging examples use slog (Go 1.21+).
|
||
metadata:
|
||
sources: "Google Style Guide, Uber Style Guide"
|
||
allowed-tools: Bash(bash:*)
|
||
---
|
||
|
||
# Go 错误处理
|
||
|
||
## 可用脚本
|
||
|
||
- **`scripts/check-errors.sh`** — 检测错误处理反模式:对 `err.Error()` 进行字符串比较、没有上下文的裸 `return err`、以及日志并返回违规。运行 `bash scripts/check-errors.sh --help` 查看选项。
|
||
|
||
在 Go 中,[错误是值](https://go.dev/blog/errors-are-values) — 它们由代码创建,也由代码消费。
|
||
|
||
## 选择错误策略
|
||
|
||
1. 系统边界(RPC、IPC、存储)?→ 使用 `%v` 包装以避免泄露内部细节
|
||
2. 调用者需要匹配特定条件?→ 哨兵或类型化错误,使用 `%w` 包装
|
||
3. 调用者只需要调试上下文?→ `fmt.Errorf("...: %w", err)`
|
||
4. 叶子函数,无需包装?→ 直接返回错误
|
||
|
||
**默认**:使用 `%w` 包装,并将其放在格式字符串的末尾。
|
||
|
||
---
|
||
|
||
## 核心规则
|
||
|
||
### 永不返回具体错误类型
|
||
|
||
**永不从导出函数返回具体错误类型** — 具体的 `nil` 指针可能变成非 nil 接口:
|
||
|
||
```go
|
||
// 不好:具体类型可能导致微妙的 bug
|
||
func Bad() *os.PathError { /*...*/ }
|
||
|
||
// 好:始终返回 error 接口
|
||
func Good() error { /*...*/ }
|
||
```
|
||
|
||
### 错误字符串
|
||
|
||
错误字符串**不应**大写,也**不应**以标点符号结尾。例外:导出名称、专有名词或缩写。
|
||
|
||
```go
|
||
// 不好
|
||
err := fmt.Errorf("Something bad happened.")
|
||
|
||
// 好
|
||
err := fmt.Errorf("something bad happened")
|
||
```
|
||
|
||
对于显示的消息(日志、测试失败、API 响应),大写是适当的。
|
||
|
||
### 出错时的返回值
|
||
|
||
当函数返回错误时,调用者必须将所有非错误返回值视为未指定,除非有明确文档说明。
|
||
|
||
**提示**:接受 `context.Context` 的函数通常应返回 `error`,以便调用者判断上下文是否被取消。
|
||
|
||
---
|
||
|
||
## 处理错误
|
||
|
||
遇到错误时,做出**深思熟虑的选择** — 不要用 `_` 丢弃:
|
||
|
||
1. **立即处理** — 解决错误并继续
|
||
2. **返回给调用者** — 可选择用上下文包装
|
||
3. **在特殊情况下** — `log.Fatal` 或 `panic`
|
||
|
||
有意忽略时:添加注释说明原因。
|
||
|
||
```go
|
||
n, _ := b.Write(p) // 永不返回非 nil 错误
|
||
```
|
||
|
||
对于相关的并发操作,使用 [`errgroup`](https://pkg.go.dev/golang.org/x/sync/errgroup):
|
||
|
||
```go
|
||
g, ctx := errgroup.WithContext(ctx)
|
||
g.Go(func() error { return task1(ctx) })
|
||
g.Go(func() error { return task2(ctx) })
|
||
if err := g.Wait(); err != nil { return err }
|
||
```
|
||
|
||
### 避免带内错误
|
||
|
||
不要返回 `-1`、`nil` 或空字符串来表示错误。使用多返回值:
|
||
|
||
```go
|
||
// 不好:带内错误值
|
||
func Lookup(key string) int // 缺失时返回 -1
|
||
|
||
// 好:显式的 error 或 ok 值
|
||
func Lookup(key string) (string, bool)
|
||
```
|
||
|
||
这可以防止调用者写出 `Parse(Lookup(key))` — 它会导致编译时错误,因为 `Lookup(key)` 有 2 个输出。
|
||
|
||
---
|
||
|
||
## 错误流程
|
||
|
||
在正常代码之前处理错误。提前返回使正常路径保持无缩进:
|
||
|
||
```go
|
||
// 好:错误优先,正常代码无缩进
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// 正常代码
|
||
```
|
||
|
||
**错误只处理一次** — 记录日志或返回,不要两者都做:
|
||
|
||
```
|
||
遇到错误?
|
||
├─ 调用者可以采取行动?→ 返回(通过 %w 附带上下文)
|
||
├─ 在调用链顶部?→ 记录日志并处理
|
||
└─ 都不是?→ 以适当级别记录日志,继续执行
|
||
```
|
||
|
||
> 在组织复杂的错误流程、决定记录日志还是返回、实现一次处理模式、或选择结构化日志级别时,请阅读 [references/ERROR-FLOW.md](references/ERROR-FLOW.md)。
|
||
|
||
---
|
||
|
||
## 错误类型
|
||
|
||
> **建议**:推荐的最佳实践。
|
||
|
||
| 调用者需要匹配? | 消息类型 | 使用方式 |
|
||
|-----------------|---------|---------|
|
||
| 否 | 静态 | `errors.New("message")` |
|
||
| 否 | 动态 | `fmt.Errorf("msg: %v", val)` |
|
||
| 是 | 静态 | `var ErrFoo = errors.New("...")` |
|
||
| 是 | 动态 | 自定义 `error` 类型 |
|
||
|
||
**默认**:使用 `fmt.Errorf("...: %w", err)` 包装。升级为哨兵以使用 `errors.Is()`,升级为自定义类型以使用 `errors.As()`。
|
||
|
||
> 在定义哨兵错误、创建自定义错误类型、或为包 API 选择错误策略时,请阅读 [references/ERROR-TYPES.md](references/ERROR-TYPES.md)。
|
||
|
||
---
|
||
|
||
## 错误包装
|
||
|
||
> **建议**:推荐的最佳实践。
|
||
|
||
- **使用 `%v`**:在系统边界、用于日志记录、隐藏内部细节
|
||
- **使用 `%w`**:保留错误链以供 `errors.Is`/`errors.As` 使用
|
||
|
||
**关键规则**:将 `%w` 放在末尾。添加调用者没有的上下文。如果注释没有增加信息,直接返回 `err`。
|
||
|
||
> 在决定使用 %v 还是 %w、跨包边界包装错误、或添加上下文信息时,请阅读 [references/WRAPPING.md](references/WRAPPING.md)。
|
||
|
||
> **验证**:实现错误处理后,运行 `bash scripts/check-errors.sh` 检测常见的反模式。然后运行 `go vet ./...` 捕获其他问题。
|
||
|
||
---
|
||
|
||
## 相关技能
|
||
|
||
- **错误命名**:在命名哨兵错误(`ErrFoo`)或自定义错误类型时,参见 [go-naming](../go-naming/SKILL.md)
|
||
- **测试错误**:在使用 `errors.Is`/`errors.As` 测试错误语义或编写错误检查辅助函数时,参见 [go-testing](../go-testing/SKILL.md)
|
||
- **Panic 处理**:在决定 panic 还是返回错误、或编写 recover 守卫时,参见 [go-defensive](../go-defensive/SKILL.md)
|
||
- **守卫子句**:在组织提前返回的错误流程或减少嵌套时,参见 [go-control-flow](../go-control-flow/SKILL.md)
|
||
- **日志决策**:在选择日志级别、配置结构化日志、或决定日志消息中包含什么上下文时,参见 [go-logging](../go-logging/SKILL.md)
|