Go 语言入门教程
哨兵错误与自定义错误
本教程共 80 篇 · 第 53 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
GoGo 入门教程错误处理哨兵错误自定义错误错误类型
53. 哨兵错误与自定义错误
本节目标:学会用哨兵错误模式预定义错误,实现自定义错误类型携带更多信息,掌握错误包装的最佳实践。
哨兵错误
哨兵错误就是预定义的全局错误变量,调用方可以用 errors.Is 检查:
package main
import (
"errors"
"fmt"
)
// 哨兵错误:预定义的全局变量
var (
ErrNotFound = errors.New("not found")
ErrPermission = errors.New("permission denied")
ErrTimeout = errors.New("timeout")
)
func GetUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return "Alice", nil
}
func main() {
_, err := GetUser(-1)
if errors.Is(err, ErrNotFound) {
fmt.Println("用户不存在")
} else if err != nil {
fmt.Println("其他错误:", err)
}
}
标准库里大量使用了哨兵错误。比如 os 包:
var (
ErrNotExist = errors.New("file does not exist")
ErrPermission = errors.New("permission denied")
ErrClosed = errors.New("use of closed file connection")
)
Note哨兵错误用
var声明,不用const。因为 error 是接口类型,不能用 const 声明。
哨兵错误的优点
- 调用方可以精确判断错误类型
- 错误信息集中管理,修改方便
- 配合
errors.Is能穿透错误包装
哨兵错误的注意点
Warning哨兵错误的值本身不重要,重要的是它的身份(identity)。调用方用
errors.Is比较,不看错误消息。所以不要靠字符串匹配来判断错误。
// 坏:靠字符串匹配
if err.Error() == "not found" { ... }
// 好:靠 errors.Is
if errors.Is(err, ErrNotFound) { ... }
自定义错误类型
哨兵错误只能表达「是什么错误」,不能携带额外信息。如果错误需要附带数据(比如错误码、出错位置等),用自定义错误类型:
package main
import (
"errors"
"fmt"
)
// 自定义错误类型
type ValidationError struct {
Field string // 哪个字段出错
Value any // 出错的值
Message string // 错误描述
}
// 实现 error 接口
func (e *ValidationError) Error() string {
return fmt.Sprintf("验证失败 [%s]: %s (值: %v)", e.Field, e.Message, e.Value)
}
func ValidateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Value: age,
Message: "年龄不能为负数",
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Value: age,
Message: "年龄不合理",
}
}
return nil
}
func main() {
err := ValidateAge(-5)
// 用 errors.As 提取自定义错误
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Println("字段:", valErr.Field) // age
fmt.Println("值:", valErr.Value) // -5
fmt.Println("描述:", valErr.Message) // 年龄不能为负数
}
}
自定义错误类型的好处:
- 携带结构化信息(字段、值、错误码等)
- 调用方可以用
errors.As提取详细信息 - 可以定义额外方法
带错误码的自定义错误
type AppError struct {
Code int // 错误码
Message string // 错误信息
}
func (e *AppError) Error() string {
return fmt.Sprintf("[ERR-%d] %s", e.Code, e.Message)
}
var (
ErrInvalidInput = &AppError{Code: 400, Message: "无效输入"}
ErrUnauthorized = &AppError{Code: 401, Message: "未授权"}
ErrInternal = &AppError{Code: 500, Message: "内部错误"}
)
func DoSomething(token string) error {
if token == "" {
return ErrUnauthorized
}
return nil
}
包装自定义错误
自定义错误和 %w 包装可以配合使用:
package main
import (
"errors"
"fmt"
)
var ErrUserNotFound = errors.New("user not found")
type User struct {
ID int
Name string
}
func FindUser(id int) (*User, error) {
if id == 0 {
return nil, fmt.Errorf("查询用户 %d: %w", id, ErrUserNotFound)
}
return &User{ID: id, Name: "Alice"}, nil
}
func main() {
_, err := FindUser(0)
// 穿透包装层检查哨兵错误
if errors.Is(err, ErrUserNotFound) {
fmt.Println("用户不存在,跳过处理")
return
}
if err != nil {
fmt.Println("其他错误:", err)
}
}
错误包装最佳实践
1. 在哪一层包装
在每层函数调用时加上自己的上下文:
func LoadConfig() error {
data, err := os.ReadFile("config.yaml")
if err != nil {
return fmt.Errorf("加载配置: %w", err)
}
// ...
}
func InitApp() error {
if err := LoadConfig(); err != nil {
return fmt.Errorf("初始化应用: %w", err) // 再包一层
}
return nil
}
// 最终错误:初始化应用: 加载配置: open config.yaml: no such file or directory
每一层加上自己这一层的语义,错误链就形成了一条调用路径,方便定位问题。
2. 包装时加什么信息
加「这一层在做什么」,不加「错误是什么」(错误本身已经有了):
// 好:加了上下文(在做什么)
return fmt.Errorf("解析JSON配置: %w", err)
// 不好:重复了错误信息
return fmt.Errorf("JSON解析失败: %v", err) // 用 %v 会丢错误链
3. 不要过度包装
不是每一行都要包装。只在有意义的边界处加上下文:
// 过度包装:太啰嗦
func Process() error {
if err := step1(); err != nil {
return fmt.Errorf("step1: %w", err)
}
if err := step2(); err != nil {
return fmt.Errorf("step2: %w", err)
}
if err := step3(); err != nil {
return fmt.Errorf("step3: %w", err)
}
return nil
}
// 适度包装:只在外层加
func Process() error {
if err := step1(); err != nil {
return err
}
if err := step2(); err != nil {
return err
}
if err := step3(); err != nil {
return fmt.Errorf("处理失败: %w", err) // 只在最后包装
}
return nil
}
Tip错误包装的度需要经验。原则是:让错误链能帮你定位问题,但不要让信息爆炸。每个包装层加一句有意义的上下文就够了。
小结
哨兵错误和自定义错误类型:
- 哨兵错误:全局
var声明的预定义错误,用errors.Is检查 - 自定义错误类型:实现
Error() string方法,携带结构化信息 - 用
errors.As提取自定义错误的字段 - 包装时加「在做什么」的上下文,不过度包装
- 哨兵错误和自定义类型可以配合
%w一起用
下一章讲错误处理策略,搞懂什么时候用 error、什么时候用 panic。