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
+168
View File
@@ -0,0 +1,168 @@
---
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)
@@ -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)` |
| 日志 | 不要既记录日志又返回;使用适当的日志级别 |
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.0.0"
SCRIPT_NAME="$(basename "$0")"
usage() {
cat <<EOF
$SCRIPT_NAME v$VERSION — Check Go code for common error handling anti-patterns
USAGE
bash $SCRIPT_NAME [options] [path]
DESCRIPTION
Scans Go source files for error handling anti-patterns:
- err.Error() used in string comparison (should use errors.Is/As)
- Bare 'return err' without wrapping context
- Errors that are both logged and returned (handle once)
Exits 0 if no issues found, 1 if anti-patterns detected, 2 on error.
OPTIONS
-h, --help Show this help message
-v, --version Show version
--json Output results as JSON
--no-bare-return Skip the bare 'return err' check (high false-positive rate)
--limit N Show at most N results (default: all)
ARGUMENTS
path Directory or file to check (default: current directory)
EXAMPLES
bash $SCRIPT_NAME
bash $SCRIPT_NAME ./pkg/api
bash $SCRIPT_NAME --json .
bash $SCRIPT_NAME --no-bare-return ./internal
EOF
}
JSON_OUTPUT=false
CHECK_BARE_RETURN=true
LIMIT=0
TARGET=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
--json) JSON_OUTPUT=true; shift ;;
--no-bare-return) CHECK_BARE_RETURN=false; shift ;;
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
*) TARGET="$1"; shift ;;
esac
done
TARGET="${TARGET:-.}"
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\t'/\\t}"
s="${s//$'\r'/}"
s="${s//$'\n'/\\n}"
printf '%s' "$s"
}
find_go_files() {
local t="$1"
if [[ -f "$t" ]]; then
echo "$t"
elif [[ -d "$t" ]]; then
find "$t" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
else
local dir="${t%%/...}"
dir="${dir:-.}"
if [[ -d "$dir" ]]; then
find "$dir" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
else
echo "error: path not found: $t" >&2
exit 2
fi
fi
}
FINDINGS=()
add_finding() {
local file="$1" line="$2" rule="$3" message="$4"
FINDINGS+=("${file}:${line}|${rule}|${message}")
}
# Rule 1: err.Error() in string comparison
check_string_error_comparison() {
local file="$1"
local line_num=0
while IFS= read -r line; do
line_num=$((line_num + 1))
# Pattern: err.Error() == "..." or err.Error() != "..."
pat='\.Error\(\)[[:space:]]*(==|!=)[[:space:]]*\"'
if [[ "$line" =~ $pat ]]; then
add_finding "$file" "$line_num" "string-error-compare" \
"comparing err.Error() to string; use errors.Is() or errors.As() instead"
fi
# Pattern: strings.Contains(err.Error(), "...")
pat_contains='strings\.Contains\(.*\.Error\(\)'
if [[ "$line" =~ $pat_contains ]]; then
add_finding "$file" "$line_num" "string-error-compare" \
"using strings.Contains on err.Error(); use errors.Is() or errors.As() instead"
fi
# Pattern: "..." == err.Error()
pat='\"[^\"]*\"[[:space:]]*(==|!=)[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*\.Error\(\)'
if [[ "$line" =~ $pat ]]; then
add_finding "$file" "$line_num" "string-error-compare" \
"comparing string to err.Error(); use errors.Is() or errors.As() instead"
fi
done < "$file"
}
# Rule 2: Bare return err (no wrapping)
check_bare_return_err() {
local file="$1"
local line_num=0
local in_error_block=false
while IFS= read -r line; do
line_num=$((line_num + 1))
# Detect if err != nil { block
pat='if[[:space:]]+(.*err[[:space:]]*(!=|==)[[:space:]]*nil|err[[:space:]]*:=)'
if [[ "$line" =~ $pat ]]; then
in_error_block=true
fi
# Check for bare "return err" that is not wrapped
pat='^[[:space:]]*return[[:space:]]+(.*,)?[[:space:]]*err[[:space:]]*$'
if $in_error_block && [[ "$line" =~ $pat ]]; then
# Exclude single-line functions and main error handlers
# Only flag if the return is just "err" (not fmt.Errorf wrapped)
local trimmed
trimmed=$(echo "$line" | sed 's/^[[:space:]]*//')
if [[ "$trimmed" == "return err" ]]; then
add_finding "$file" "$line_num" "bare-return-err" \
"bare 'return err' without wrapping context; consider fmt.Errorf('...: %w', err)"
fi
fi
# Reset error block tracking on closing brace at same indentation
pat_close='^[[:space:]]*\}[[:space:]]*$'
if $in_error_block && [[ "$line" =~ $pat_close ]]; then
in_error_block=false
fi
done < "$file"
}
# Rule 3: Log-and-return (handle errors once)
check_log_and_return() {
local file="$1"
local line_num=0
local prev_lines=()
while IFS= read -r line; do
line_num=$((line_num + 1))
prev_lines+=("$line")
# Keep a small window to detect log followed by return err
if [[ ${#prev_lines[@]} -gt 5 ]]; then
prev_lines=("${prev_lines[@]:1}")
fi
# Check if current line is 'return ... err' and a recent line logged the error
pat='^[[:space:]]*return[[:space:]]+(.*,)?[[:space:]]*err'
if [[ "$line" =~ $pat ]]; then
local window_size=${#prev_lines[@]}
for ((i=0; i<window_size-1; i++)); do
local prev="${prev_lines[$i]}"
# Match log.Print/Printf/Println/Error/Errorf/Warn/Warnf with err
pat_log1='(log\.|logger\.|slog\.)[a-zA-Z]*\(.*[^a-zA-Z]err[^a-zA-Z]'
pat_log2='(log\.|logger\.|slog\.)[a-zA-Z]*\(err[,\)]'
if [[ "$prev" =~ $pat_log1 ]] || \
[[ "$prev" =~ $pat_log2 ]]; then
local log_line=$((line_num - window_size + 1 + i))
add_finding "$file" "$log_line" "log-and-return" \
"error is both logged (line $log_line) and returned (line $line_num); handle errors once"
break
fi
done
fi
done < "$file"
}
FILES=()
while IFS= read -r f; do
[[ -n "$f" ]] && FILES+=("$f")
done < <(find_go_files "$TARGET")
if [[ ${#FILES[@]} -eq 0 ]]; then
if $JSON_OUTPUT; then
echo '{"findings":[],"count":0,"status":"no_go_files"}'
else
echo "No Go files found in: $TARGET"
fi
exit 0
fi
for file in "${FILES[@]}"; do
check_string_error_comparison "$file"
if $CHECK_BARE_RETURN; then
check_bare_return_err "$file"
fi
check_log_and_return "$file"
done
# Truncation
TOTAL=${#FINDINGS[@]}
TRUNCATED=false
if [[ $LIMIT -gt 0 && $TOTAL -gt $LIMIT ]]; then
FINDINGS=("${FINDINGS[@]:0:$LIMIT}")
TRUNCATED=true
fi
if $JSON_OUTPUT; then
echo "{"
echo ' "findings": ['
first=true
for entry in "${FINDINGS[@]+"${FINDINGS[@]}"}"; do
IFS='|' read -r location rule message <<< "$entry"
file="${location%%:*}"
line="${location#*:}"
$first || echo ","
first=false
printf ' {"file":"%s","line":%s,"rule":"%s","message":"%s"}' \
"$(json_escape "$file")" "$line" "$(json_escape "$rule")" "$(json_escape "$message")"
done
echo ""
echo " ],"
printf ' "total": %d,\n' "$TOTAL"
printf ' "truncated": %s\n' "$TRUNCATED"
echo "}"
else
if [[ $TOTAL -eq 0 ]]; then
echo "No error handling anti-patterns found."
exit 0
fi
echo "Error handling anti-patterns found:"
echo ""
for entry in "${FINDINGS[@]}"; do
IFS='|' read -r location rule message <<< "$entry"
printf " %s [%s] %s\n" "$location" "$rule" "$message"
done
if $TRUNCATED; then
echo " ... and $((TOTAL - LIMIT)) more (use --limit to adjust)"
fi
echo ""
echo "Total: $TOTAL finding(s)"
fi
if [[ $TOTAL -gt 0 ]]; then
exit 1
fi
exit 0