Go 错误处理
error 接口、包装与解包、errors.Is/As、panic 与 recover。
Key Points错误是值,用 `%w` 包装保留链条,用 `errors.Is/As` 判断而不是比较字符串。
error 是接口
Go
type error interface {
Error() string
}Go
import "errors"
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
r, err := divide(1, 0)
if err != nil {
fmt.Println("错误:", err)
}Output
错误: 除数不能为零自定义错误
Go
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %d 不存在", e.Resource, e.ID)
}
func findUser(id int) error {
return &NotFoundError{Resource: "用户", ID: id}
}
err := findUser(7)
fmt.Println(err)Output
用户 7 不存在包装与解包
Go
var ErrNotFound = errors.New("未找到")
func loadConfig() error {
return fmt.Errorf("读取配置失败: %w", ErrNotFound) // %w 包装
}
func main() {
err := loadConfig()
fmt.Println(err) // 完整信息
fmt.Println(errors.Is(err, ErrNotFound)) // true
// 取出原始错误
fmt.Println(errors.Unwrap(err))
}Output
读取配置失败: 未找到
true
未找到%w and %v differ in one way: %w keeps the wrapped error in the chain so errors.Is can find it. Use %w whenever you need to test the error later.
errors.Is 与 errors.As
Go
// Is:判断是不是某个「哨兵错误」
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在")
}
// As:判断是不是某个「错误类型」,并取出来用
var nf *NotFoundError
if errors.As(err, &nf) {
fmt.Println("找不到", nf.Resource, nf.ID)
}| Function | Compares | Use when |
|---|---|---|
errors.Is | 错误值(哨兵) | ErrNotFound 这类预定义错误 |
errors.As | 错误类型 | 要访问自定义错误的字段 |
== | 直接比较 | 只在不包装时有效 |
Never test with
err.Error() == "not found". A wording change breaks it, and wrapping lengthens the message. One of the most common Go review comments.panic 与 recover
Go
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
return a / b, nil // b=0 时 panic
}
r, err := safeDivide(1, 0)
fmt.Println(r, err)
r, err = safeDivide(10, 2)
fmt.Println(r, err)Output
0 panic: runtime error: integer divide by zero
5 <nil>panic is for the program cannot continue (out of range, nil deref, failed init); error is for expected failures (missing file, validation). In business code 99% of errors should be error.
错误处理的三条实践
- 不要忽略 ——
_ = doSomething()是技术债,至少记日志 - 加上下文 ——
fmt.Errorf("加载用户 %d: %w", id, err) - 只处理一次 —— 要么记录要么返回,不要既记录又返回(日志会重复)
Go
// 反例:日志重复,一层层打同一句话
func a() error {
err := b()
if err != nil {
log.Println(err)
return err
}
return nil
}
// 正例:底层包装上下文,顶层统一记录
func loadUser(id int) error {
u, err := db.Find(id)
if err != nil {
return fmt.Errorf("查询用户 %d: %w", id, err)
}
_ = u
return nil
}error 是接口
Go
type error interface {
Error() string
}Go
import "errors"
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
r, err := divide(1, 0)
if err != nil {
fmt.Println("错误:", err)
}Output
错误: 除数不能为零自定义错误
Go
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %d 不存在", e.Resource, e.ID)
}
func findUser(id int) error {
return &NotFoundError{Resource: "用户", ID: id}
}
err := findUser(7)
fmt.Println(err)Output
用户 7 不存在包装与解包
Go
var ErrNotFound = errors.New("未找到")
func loadConfig() error {
return fmt.Errorf("读取配置失败: %w", ErrNotFound) // %w 包装
}
func main() {
err := loadConfig()
fmt.Println(err) // 完整信息
fmt.Println(errors.Is(err, ErrNotFound)) // true
// 取出原始错误
fmt.Println(errors.Unwrap(err))
}Output
读取配置失败: 未找到
true
未找到%w 和 %v 的差别只有一点:%w 会把被包装的错误留在链上,errors.Is 才能沿着链找到它。要判断错误类型就必须用 %w。
errors.Is 与 errors.As
Go
// Is:判断是不是某个「哨兵错误」
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在")
}
// As:判断是不是某个「错误类型」,并取出来用
var nf *NotFoundError
if errors.As(err, &nf) {
fmt.Println("找不到", nf.Resource, nf.ID)
}| 函数 | 比较什么 | 何时用 |
|---|---|---|
errors.Is | 错误值(哨兵) | ErrNotFound 这类预定义错误 |
errors.As | 错误类型 | 要访问自定义错误的字段 |
== | 直接比较 | 只在不包装时有效 |
不要用
err.Error() == "未找到" 判断错误。文案一改就失效,而且包装后的错误文案会变长。这是 Go 代码评审里最常见的意见之一。panic 与 recover
Go
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
return a / b, nil // b=0 时 panic
}
r, err := safeDivide(1, 0)
fmt.Println(r, err)
r, err = safeDivide(10, 2)
fmt.Println(r, err)Output
0 panic: runtime error: integer divide by zero
5 <nil>panic 用于程序无法继续的情况(数组越界、空指针、初始化失败),error 用于可预期的失败(文件不存在、校验不通过)。业务代码里 99% 的错误都应该是 error。
错误处理的三条实践
- 不要忽略 ——
_ = doSomething()是技术债,至少记日志 - 加上下文 ——
fmt.Errorf("加载用户 %d: %w", id, err) - 只处理一次 —— 要么记录要么返回,不要既记录又返回(日志会重复)
Go
// 反例:日志重复,一层层打同一句话
func a() error {
err := b()
if err != nil {
log.Println(err)
return err
}
return nil
}
// 正例:底层包装上下文,顶层统一记录
func loadUser(id int) error {
u, err := db.Find(id)
if err != nil {
return fmt.Errorf("查询用户 %d: %w", id, err)
}
_ = u
return nil
}