Go 入门(16):错误处理进阶——fmt.Errorf 与 errors 包
更新时间:2026-09-01。本文是
languages/go/beginner/入门层第 16 篇,接 命令行参数。Go 1.13 引入错误包装机制,%w包裹上下文,errors.Is和errors.As遍历错误链,让错误处理更灵活。
本文要回答的问题
fmt.Errorf("...: %w", err)的%w做什么?和%v有什么区别?errors.Is和errors.As怎么用?什么时候用哪个?- 自定义错误类型怎么写?什么时候需要自定义错误?
- 错误包装和直接返回错误,有什么区别?
一、直接返回 vs 包装错误
go
// 方式一:直接返回,不包装
if err != nil {
return err // 调用者只知道底层错误,不知道上下文
}
// 方式二:添加上下文,用 %v
if err != nil {
return fmt.Errorf("read config: %v", err) // 添加了上下文,但丢失了原错误类型
}
// 方式三:包装错误,用 %w
if err != nil {
return fmt.Errorf("read config: %w", err) // 添加上下文,保留原错误
}| 方式 | 效果 | 是否保留原错误 |
|---|---|---|
return err | 直接透传,没上下文 | 保留 |
fmt.Errorf("...: %v", err) | 添加上下文,但变成字符串 | ❌ 丢失类型 |
fmt.Errorf("...: %w", err) | 添加上下文,保留原错误 | ✅ 保留 |
何时用哪种?
- 底层错误(如
os.Open的ErrNotExist)需要被调用者识别时 → 用%w包装 - 调用者只需要知道"失败了" → 用
%v或%w都可以 - 错误信息只是给人看的,不需要程序判断 → 用
%v足够
二、errors.Is:检查错误链里的某个错误
go
import "errors"
// 打开文件时,如果文件不存在不 panic,而是做别的
_, err := os.Open("config.json")
if errors.Is(err, os.ErrNotExist) {
// 文件不存在,用默认配置
} else if err != nil {
// 其他错误,返回
log.Fatal(err)
}errors.Is 会遍历错误链(包装过的错误),检查链中是否有任何一个等于 target。
go
// 自定义哨兵错误
var ErrNotFound = errors.New("not found")
func FindUser(id int) (*User, error) {
return nil, fmt.Errorf("database: %w", ErrNotFound)
}
_, err := FindUser(42)
if errors.Is(err, ErrNotFound) {
// 匹配 "not found"
}三、errors.As:把错误转换成特定类型
go
// 自定义错误类型
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("code=%d: %s", e.Code, e.Message)
}
func doSomething() error {
return &MyError{Code: 400, Message: "bad request"}
}
err := doSomething()
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Println(myErr.Code) // 400
fmt.Println(myErr.Message) // bad request
}errors.As 遍历错误链,找到第一个可以赋值给目标类型的错误。和 errors.Is 的区别:
errors.Is→ 检查是否等于某个错误(哨兵错误)errors.As→ 检查是否是某个类型(自定义错误类型)
四、自定义错误类型
什么时候需要自定义错误?
go
// 1. 调用者需要根据错误码做不同处理
type APIError struct {
StatusCode int
Body string
}
func (e *APIError) Error() string {
return fmt.Sprintf("API error %d: %s", e.StatusCode, e.Body)
}
// 2. 错误需要携带额外信息
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}自定义错误实现 Error() string 方法,就满足 error 接口。如果要让 errors.Is 和 errors.As 能识别,记得用指针接收者。
五、哨兵错误:errors.New
go
var ErrNotFound = errors.New("user not found")
var ErrPermissionDenied = errors.New("permission denied")哨兵错误是固定的错误值,调用者用 errors.Is 判断。命名惯例:前缀 Err。
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
用 %v 包装后想检查错误类型 | errors.Is 找不到 | 用 %w 包装才能保留错误链 |
多层包装忘记 %w | 错误链断了 | 每一层包装都用 %w |
| 自定义错误用值接收者 | errors.As 找不到 | 用指针接收者实现 Error() |
到处用 %w | 不需要程序判断的错误也包装 | 只包装需要被识别的错误 |
相关与延伸
下一篇:panic 与 recover——什么时候用,怎么 recover;Go 错误处理哲学和 C 的 errno 对比,C 错误处理;错误包装的底层实现,进阶层讲。
一句话总结
Go 错误处理进阶:fmt.Errorf("...: %w", err) 包装错误保留链,errors.Is 检查链中是否有某个错误(哨兵错误),errors.As 把错误转成特定类型;自定义错误类型实现 error 接口,用指针接收者;需要程序判断的错误用 %w,人看的错误用 %v 加上下文就够了。