golang检查外部包错误是否被包装的lint插件wrapcheck的使用

Golang检查外部包错误是否被包装的lint插件wrapcheck的使用

Wrapcheck是一个简单的Go语言lint工具,用于检查从外部包返回的错误是否被包装,以帮助在调试时识别错误来源。

安装

要求Go版本 >= v1.22.0

$ go install github.com/tomarrell/wrapcheck/v2/cmd/wrapcheck@v2

Wrapcheck也可以作为golangci-lint元linter的一部分使用。

配置

可以通过在本地目录或主目录中使用.wrapcheck.yaml文件来配置wrapcheck。

# 指定要忽略的签名子字符串数组
ignoreSigs:
- .Errorf(
- errors.New(
- errors.Unwrap(
- errors.Join(
- .Wrap(
- .Wrapf(
- .WithMessage(
- .WithMessagef(
- .WithStack(

# 指定额外要忽略的签名子字符串数组
extraIgnoreSigs:
- .CustomError(
- .SpecificWrap(

# 指定要忽略的签名正则表达式数组
ignoreSigRegexps:
- \\.New.*Error\(

# 指定要忽略的包glob模式数组
ignorePackageGlobs:
- encoding/*
- github.com/pkg/*

# 定义要忽略的接口正则表达式列表
ignoreInterfaceRegexps:
- ^(?i)c(?-i)ach(ing|e)

# 是否报告来自包内部的错误
reportInternalErrors: true

使用

要检查程序中的所有包:

$ wrapcheck ./...

示例

当你在调试Go程序时,看到如下错误日志:

time="2020-08-04T11:36:27+02:00" level=error error="sql: error no rows"

很难定位错误来源,特别是当你有多个类似的方法:

func (db *DB) getUserByID(userID string) (User, error) {
	sql := `SELECT * FROM user WHERE id = $1;`

	var u User
	if err := db.conn.Get(&u, sql, userID); err != nil {
		return User{}, err // wrapcheck error: error returned from external package is unwrapped
	}

	return u, nil
}

func (db *DB) getItemByID(itemID string) (Item, error) {
	sql := `SELECT * FROM item WHERE id = $1;`

	var i Item
	if err := db.conn.Get(&i, sql, itemID); err != nil {
		return Item{}, err // wrapcheck error: error returned from external package is unwrapped
	}

	return i, nil
}

解决方法是在返回外部包错误时包装它:

func (db *DB) getUserByID(userID string) (User, error) {
	sql := `SELECT * FROM user WHERE id = $1;`

	var u User
	if err := db.Conn.Get(&u, sql, userID); err != nil {
		return User{}, fmt.Errorf("failed to get user by ID: %v", err) // No error!
	}

	return u, nil
}

func (db *DB) getItemByID(itemID string) (Item, error) {
	sql := `SELECT * FROM item WHERE id = $1;`

	var i Item
	if err := db.Conn.Get(&i, sql, itemID); err != nil {
		return Item{}, fmt.Errorf("failed to get item by ID: %v", err) // No error!
	}

	return i, nil
}

现在你的日志会更具有描述性,更容易定位错误来源:

time="2020-08-04T11:36:27+02:00" level=error error="failed to get user by ID: sql: error no rows"

为什么需要包装错误

Go中的错误是简单的值,它们只包含满足接口所需的最小信息:

type Error interface {
  Error() string
}

这是一个很好的特性,但也可能是一个限制。特别是当你试图识别程序中错误的来源时。

从Go 1.13开始,使用fmt.Errorf(...)包装错误是Go中推荐的方式来组合错误以添加附加信息。

解决方案

在调用可能返回错误的外部库时,我们可以包装错误以添加有关调用点的附加信息。

例如:

func (db *DB) createUser(name, email, city string) error {
  sql := `INSERT INTO customer (name, email, city) VALUES ($1, $2, $3);`

  if _, err := tx.Exec(sql, name, email, city); err != nil {
    // %v verb preferred to prevent error becoming part of external API
    return fmt.Errorf("failed to insert user: %v", err)
  }

  return nil
}

这个解决方案允许你添加上下文,这些上下文将被传递给调用者,使得在调试时更容易识别来源。

贡献

与大多数静态分析工具一样,这个linter可能会遗漏一些晦涩的情况。如果你遇到应该覆盖但没有覆盖的情况,请提交一个问题,包括该情况的最小可重现示例。


更多关于golang检查外部包错误是否被包装的lint插件wrapcheck的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang检查外部包错误是否被包装的lint插件wrapcheck的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang 错误包装检查工具 wrapcheck

wrapcheck 是一个用于检查 Go 代码中错误处理是否适当包装外部错误的静态分析工具(linter)。它确保从外部包返回的错误在被返回给调用者之前被适当地包装,以提供更多上下文信息。

安装 wrapcheck

可以通过以下命令安装 wrapcheck:

go install github.com/tomarrell/wrapcheck/v2/cmd/wrapcheck@latest

基本使用

wrapcheck 可以单独运行或集成到 golangci-lint 中:

# 单独运行
wrapcheck ./...

# 作为 golangci-lint 的一部分
golangci-lint run --enable=wrapcheck

配置

wrapcheck 支持通过 .wrapcheck.yaml 文件进行配置。示例配置:

# 需要检查的包列表(默认检查所有外部包)
ignorePackageGlobs:
  - "encoding/*"
  - "io"

# 允许不包装的错误类型
ignoreInterfaceRegexps:
  - "^error$"
  - "^runtime\\.errorString$"

# 忽略特定函数返回的错误
ignoreFunctionRegexps:
  - "\\.Close\\(\\)$"
  - "\\.Write\\(\\)$"

示例代码

不符合 wrapcheck 要求的代码

package main

import (
	"database/sql"
	"fmt"
	"os"
)

func getUserFromDB(db *sql.DB, id int) (*User, error) {
	var user User
	err := db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
	if err != nil {
		return nil, err // wrapcheck 会报错:外部包错误未被包装
	}
	return &user, nil
}

func readConfig() ([]byte, error) {
	data, err := os.ReadFile("config.json")
	return data, err // wrapcheck 会报错:外部包错误未被包装
}

符合 wrapcheck 要求的代码

package main

import (
	"database/sql"
	"fmt"
	"os"
)

func getUserFromDB(db *sql.DB, id int) (*User, error) {
	var user User
	err := db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
	if err != nil {
		return nil, fmt.Errorf("failed to get user %d: %w", id, err) // 使用 %w 包装错误
	}
	return &user, nil
}

func readConfig() ([]byte, error) {
	data, err := os.ReadFile("config.json")
	if err != nil {
		return nil, fmt.Errorf("failed to read config: %w", err) // 使用 %w 包装错误
	}
	return data, nil
}

为什么使用 wrapcheck

  1. 更好的错误上下文:包装错误可以提供更多关于错误发生位置的上下文信息
  2. 更容易调试:包装后的错误栈可以更清晰地追踪问题源头
  3. 一致性:确保整个代码库中的错误处理方式一致
  4. 兼容性:使用 %w 包装的错误仍然可以通过 errors.Iserrors.As 进行检查

与其他工具集成

wrapcheck 可以很好地与其他 Go 工具集成:

  1. golangci-lint:在配置文件中启用 wrapcheck
  2. pre-commit:在提交前运行检查
  3. CI/CD 管道:作为构建过程的一部分运行

替代方案

如果你不想使用 wrapcheck,也可以考虑其他类似工具:

  1. errcheck:检查是否处理了所有错误
  2. errorlint:检查错误处理的最佳实践
  3. go-consistent:检查代码风格一致性,包括错误处理

wrapcheck 专注于错误包装这一特定方面,是 Go 错误处理工具链中有价值的补充。

回到顶部