在Golang的编程体系中,错误处理是非常核心的设计部分,errors包作为标准库提供的错误处理工具,是开发者日常开发中使用频率极高的组件。它提供了创建简单错误、判断错误类型、包装错误等基础能力,配合Golang内置的error接口,形成了完整的错误处理生态。
error接口基础
Golang中所有错误类型都实现了内置的error接口,该接口仅包含一个Error() string方法,用于返回错误的描述信息。errors包的核心能力就是基于这个接口实现的。
我们先看error接口的定义:
// 内置error接口定义
type error interface {
Error() string
}
使用errors创建基础错误
errors包最常用的功能是创建简单的错误实例,通过errors.New()函数可以快速生成一个包含指定错误信息的错误对象。
基础错误创建示例
下面是一个简单的函数,当输入参数为负数时返回错误:
package main
import (
"errors"
"fmt"
)
// 计算平方根的函数,仅支持非负数输入
func sqrt(n float64) (float64, error) {
if n < 0 {
// 使用errors.New创建错误
return 0, errors.New("输入参数不能为负数")
}
// 这里简化计算逻辑,实际场景可以用math.Sqrt
return n * n, nil
}
func main() {
result, err := sqrt(-5)
if err != nil {
fmt.Println("计算失败:", err.Error())
return
}
fmt.Println("计算结果:", result)
}
运行上述代码,当传入-5时,会输出计算失败: 输入参数不能为负数,这就是最基础的errors使用场景。
errors.Is判断错误类型
在Golang 1.13之后,errors包新增了errors.Is()函数,用于判断错误链中是否包含目标错误,解决了之前直接比较错误地址不准确的问题。
比如我们定义一个通用的错误变量,然后在函数中返回这个错误,使用errors.Is()判断:
package main
import (
"errors"
"fmt"
)
// 定义通用错误变量
var ErrInvalidParam = errors.New("参数不合法")
func validateParam(param int) error {
if param == 0 {
return ErrInvalidParam
}
return nil
}
func main() {
err := validateParam(0)
// 使用errors.Is判断错误是否为ErrInvalidParam
if errors.Is(err, ErrInvalidParam) {
fmt.Println("捕获到参数不合法错误")
}
}
这种方式比直接写err == ErrInvalidParam更可靠,因为即使错误被包装过,errors.Is()也能正确识别。
errors.As提取错误具体类型
如果我们需要获取错误的具体类型,提取其中的自定义字段,可以使用errors.As()函数。首先需要定义自定义错误类型:
package main
import (
"errors"
"fmt"
)
// 自定义错误类型,包含错误码和描述
type CustomError struct {
Code int
Message string
}
// 实现error接口的Error方法
func (e *CustomError) Error() string {
return fmt.Sprintf("错误码:%d, 描述:%s", e.Code, e.Message)
}
func doSomething() error {
// 返回自定义错误
return &CustomError{
Code: 1001,
Message: "资源不存在",
}
}
func main() {
err := doSomething()
var customErr *CustomError
// 使用errors.As提取自定义错误
if errors.As(err, &customErr) {
fmt.Printf("捕获到自定义错误,错误码:%d, 描述:%sn", customErr.Code, customErr.Message)
}
}
错误包装与errors.Unwrap
errors包还支持错误包装,通过fmt.Errorf()配合%w占位符可以包装底层错误,形成错误链,方便排查问题根源。errors.Unwrap()可以获取被包装的底层错误。
package main
import (
"errors"
"fmt"
)
var ErrDBConnect = errors.New("数据库连接失败")
func queryData() error {
// 模拟底层错误,包装后返回
return fmt.Errorf("查询数据失败: %w", ErrDBConnect)
}
func main() {
err := queryData()
fmt.Println("顶层错误:", err.Error())
// 获取被包装的底层错误
unwrapErr := errors.Unwrap(err)
if unwrapErr != nil {
fmt.Println("底层错误:", unwrapErr.Error())
}
// 判断错误链中是否包含ErrDBConnect
if errors.Is(err, ErrDBConnect) {
fmt.Println("错误根源是数据库连接失败")
}
}
使用注意事项
- 不要频繁使用
errors.New()在函数内部创建临时错误变量,对于通用的错误建议定义为包级变量,方便统一判断。 - 错误描述尽量简洁清晰,不要包含敏感信息,比如用户密码、内部接口地址等。
- 包装错误时,上层错误的描述要能体现当前操作的上下文,底层错误保留原始错误信息,方便问题定位。
- 处理错误时,不要忽略返回的error,即使当前逻辑不需要处理,也建议至少打印日志,避免错误被静默吞掉。
常见错误用法避坑
很多新手会犯的错误是直接比较错误的字符串内容来判断错误类型,比如if err.Error() == "参数不合法",这种方式非常不可靠,因为错误描述可能会被修改,而且不同语言环境下描述也可能不同,应该优先使用errors.Is()或者errors.As()来判断。
另外,不要返回nil的错误时同时返回非nil的其他值,Golang的惯例是当函数返回错误时,其他返回值应该处于未定义状态,调用方不应该依赖这些返回值。