2. Go 错误处理基础:error 接口、errors.New、fmt.Errorf、自定义错误类型
在 Go 语言中,错误处理是程序健壮性的基石。Go 没有传统面向对象语言中的异常捕获机制(try-catch),而是采用显式的错误返回值设计。本章将系统讲解 Go 错误处理的核心概念:error 接口、标准库提供的错误构造方法以及自定义错误类型的实现。
2.1 error 接口:一切错误的契约
Go 语言内置的 error 是一个预声明接口,定义在 builtin 包中。任何实现了该接口的类型都可以作为错误值传递。
// error 接口定义
type error interface {
Error() string
}
该接口仅包含一个 Error() 方法,返回描述错误信息的字符串。这意味着:
- 任何类型只要实现了
Error() string方法,就自动满足error接口。 - 函数通过返回
error类型来传递错误,调用方必须显式检查。 nil表示没有错误,非nil表示存在错误。
关键原则:Go 中的错误是值,可以像其他值一样赋值、比较、传递。错误处理是代码逻辑的一部分,而非异常路径。
2.2 errors.New:快速构造简单错误
errors.New 是标准库 errors 包提供的函数,用于创建一个包含固定错误消息的 error 值。其内部实现是一个私有类型 errorString,仅存储字符串并实现 Error() 方法。
package errors
// errorString 是 errors.New 的内部实现类型
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
// New 返回一个包含给定文本的错误
func New(text string) error {
return &errorString{text}
}
使用示例:
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("发生错误:", err)
return
}
fmt.Println("结果:", result)
}
适用场景:
errors.New 适用于静态、固定的错误消息,不需要动态格式化参数。每次调用都会分配一个新的错误实例,但通常开销可忽略。
2.3 fmt.Errorf:带格式化的错误构造
当错误消息需要包含动态信息(如变量值、上下文参数)时,fmt.Errorf 是更便捷的选择。它支持 fmt.Sprintf 的所有格式化动词,并返回 error 类型。
package main
import (
"fmt"
)
func openFile(path string) error {
// 模拟文件打开失败
return fmt.Errorf("无法打开文件 %s: 权限不足", path)
}
func main() {
err := openFile("/etc/shadow")
if err != nil {
fmt.Println(err)
// 输出: 无法打开文件 /etc/shadow: 权限不足
}
}
与 errors.New 的对比:
| 特性 | errors.New | fmt.Errorf |
|---|---|---|
| 参数 | 固定字符串 | 格式化字符串 + 可变参数 |
| 动态内容 | 不支持 | 支持 %s、%d、%v 等 |
| 性能 | 略快(无格式化开销) | 稍慢(需解析格式化模板) |
| 适用场景 | 常量错误消息 | 需要嵌入运行时值的错误 |
注意:Go 1.13 之后,
fmt.Errorf 支持 %w 动词用于错误包装(error wrapping),这将在后续章节详细讲解。在基础阶段,请先掌握 %s、%v 等常规格式化用法。
2.4 自定义错误类型:携带更多上下文
当错误需要包含结构化信息(如错误码、错误层级、内部原因)时,自定义错误类型是更好的选择。通过实现 error 接口,可以定义任意复杂的错误结构。
2.4.1 基础自定义错误
package main
import "fmt"
// ValidationError 自定义验证错误类型
type ValidationError struct {
Field string // 出错的字段名
Value interface{} // 导致错误的输入值
Message string // 错误描述
}
// Error 实现 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 || age > 150 {
return &ValidationError{
Field: "age",
Value: age,
Message: "年龄必须在 0-150 之间",
}
}
return nil
}
func main() {
err := validateAge(-5)
if err != nil {
// 类型断言获取具体错误信息
if ve, ok := err.(*ValidationError); ok {
fmt.Printf("错误字段: %s\n", ve.Field)
fmt.Printf("错误值: %v\n", ve.Value)
}
fmt.Println(err)
}
}
2.4.2 带错误码的自定义错误
在大型项目中,错误码是错误分类和国际化的重要工具。以下是一个带错误码的自定义错误实现:
package main
import "fmt"
// ErrorCode 定义错误码类型
type ErrorCode int
const (
ErrNotFound ErrorCode = 1001
ErrPermission ErrorCode = 1002
ErrInvalidInput ErrorCode = 1003
)
// AppError 带错误码的应用错误
type AppError struct {
Code ErrorCode
Message string
Err error // 内部原始错误(可选)
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// Unwrap 支持 errors.Unwrap 链式解包
func (e *AppError) Unwrap() error {
return e.Err
}
func getUser(id int) error {
return &AppError{
Code: ErrNotFound,
Message: "用户不存在",
Err: fmt.Errorf("用户 ID %d 未找到", id),
}
}
func main() {
err := getUser(42)
if err != nil {
fmt.Println(err)
// 输出: [1001] 用户不存在: 用户 ID 42 未找到
}
}
最佳实践:自定义错误类型应至少包含
Error() string 方法。如果需要支持错误链(error wrapping),应同时实现 Unwrap() error 方法。错误码建议使用常量定义,避免魔法数字。
2.5 错误处理核心流程
下图展示了 Go 错误处理的核心流程,从错误构造到调用方处理:
2.6 三种错误构造方式的选择策略
在实际开发中,应根据错误的使用场景选择合适的构造方式:
- 静态错误消息:使用
errors.New,例如errors.New("EOF")。 - 需要格式化动态值:使用
fmt.Errorf,例如fmt.Errorf("用户 %s 不存在", username)。 - 需要结构化信息或错误码:定义自定义错误类型,实现
error接口。 - 需要错误链(wrapping):使用
fmt.Errorf的%w动词或自定义Unwrap()方法(后续章节详解)。
实战建议:在项目初期就定义统一的错误码体系和自定义错误类型,避免后期大规模重构。对于库代码,优先使用
errors.New 或 fmt.Errorf 返回简单错误;对于业务代码,推荐使用带错误码的自定义错误类型。
2.7 错误检查的常见模式
Go 中检查错误的惯用模式包括:
| 模式 | 代码示例 | 说明 |
|---|---|---|
| 基本检查 | if err != nil { return err } |
最常用,直接返回错误 |
| 带日志 | if err != nil { log.Printf("操作失败: %v", err); return err } |
记录错误后返回 |
| 类型断言 | if ve, ok := err.(*ValidationError); ok { ... } |
获取自定义错误详情 |
| 错误码判断 | if ae, ok := err.(*AppError); ok && ae.Code == ErrNotFound { ... } |
根据错误码分支处理 |
| 忽略错误 | _ = doSomething() |
显式忽略(应谨慎使用) |
避坑指南:永远不要使用
_ = doSomething() 忽略错误,除非你明确知道该错误不需要处理(如 strings.Writer.Write 的返回值)。忽略错误是 Go 程序中最常见的 bug 来源之一。
2.8 本章小结
本章覆盖了 Go 错误处理的基础知识:
- error 接口:Go 错误处理的核心契约,任何实现了
Error() string的类型都是错误。 - errors.New:快速构造静态错误消息。
- fmt.Errorf:构造带格式化参数的动态错误消息。
- 自定义错误类型:通过结构体实现
error接口,携带错误码、字段名等结构化信息。 - 错误检查模式:基本检查、类型断言、错误码判断等惯用写法。