前言
Go语言以"错误就是值"(error is a value)为设计哲学,将错误处理显式化而非异常机制。这种设计虽然写起来繁琐,但让错误处理更加清晰、可控。本文深入讲解Go的错误处理机制、errors包的使用以及最佳实践。
一、Error接口
1.1 Error接口定义
type error interface {
Error() string
}
任何实现Error()方法的类型都实现了error接口:
// 自定义错误类型
type MyError struct {
Msg string
Code int
}
func (e *MyError) Error() string {
return fmt.Sprintf("错误码: %d, 消息: %s", e.Code, e.Msg)
}
func main() {
var err error = &MyError{"文件未找到", 404}
fmt.Println(err) // 输出: 错误码: 404, 消息: 文件未找到
}
1.2 创建错误的方式
import "errors"
func main() {
// 方式1:errors.New()
err1 := errors.New("这是一个错误")
// 方式2:fmt.Errorf()(可格式化)
err2 := fmt.Errorf("这是一个格式化错误: %s", "详情")
// 方式3:自定义错误类型
err3 := &ValidationError{Field: "email", Msg: "格式不正确"}
}
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("验证失败 [%s]: %s", e.Field, e.Msg)
}
二、错误处理模式
2.1 基本的错误检查
func readFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取文件失败: %w", err)
}
return data, nil
}
func main() {
data, err := readFile("test.txt")
if err != nil {
fmt.Printf("处理失败: %v\n", err)
return
}
fmt.Println(string(data))
}
2.2 哨兵错误(Sentinel Errors)
import (
"errors"
"io"
)
var (
ErrNotFound = errors.New("资源未找到")
ErrPermission = errors.New("权限不足")
ErrInvalidInput = errors.New("无效输入")
EOF = io.EOF // 标准库的哨兵错误
)
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, ErrInvalidInput
}
if id > 1000 {
return nil, ErrNotFound
}
return &User{ID: id, Name: "张三"}, nil
}
func main() {
user, err := findUser(2000)
if err != nil {
if errors.Is(err, ErrNotFound) {
fmt.Println("用户不存在")
} else if errors.Is(err, ErrInvalidInput) {
fmt.Println("输入无效")
}
return
}
fmt.Printf("找到用户: %+v\n", user)
}
2.3 错误包装
Go 1.13+ 引入了错误包装机制:
import (
"errors"
"fmt"
)
func level1() error {
return fmt.Errorf("level1 error: %w", errors.New("原始错误"))
}
func level2() error {
err := level1()
return fmt.Errorf("level2 error: %w", err)
}
func main() {
err := level2()
// errors.Is 检查错误链
fmt.Printf("err == level2: %t\n", errors.Is(err, errors.New("原始错误")))
// errors.As 获取具体类型
var customErr *CustomError
if errors.As(err, &customErr) {
fmt.Printf("获取到自定义错误: %+v\n", customErr)
}
}
2.4 errors.Is vs errors.As
import "errors"
func main() {
// errors.Is: 检查错误链中是否有匹配的哨兵错误
err := fmt.Errorf("包装: %w", fmt.Errorf("再次包装: %w", ErrNotFound))
if errors.Is(err, ErrNotFound) {
fmt.Println("找到了 ErrNotFound")
}
// errors.As: 在错误链中找到指定类型的错误
err2 := fmt.Errorf("外层: %w", &MyError{Code: 100, Msg: "自定义"})
var myErr *MyError
if errors.As(err2, &myErr) {
fmt.Printf("获取到 MyError: Code=%d, Msg=%s\n", myErr.Code, myErr.Msg)
}
}
三、自定义错误类型
3.1 结构化错误
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
Err error `json:"-"` // 嵌套错误,不序列化
Stack string `json:"-"` // 调用栈
}
func (e *Error) 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)
}
func (e *Error) Unwrap() error {
return e.Err
}
// 便捷构造函数
func NewError(code int, msg string) *Error {
return &Error{Code: code, Message: msg}
}
func WrapError(code int, msg string, err error) *Error {
return &Error{Code: code, Message: msg, Err: err}
}
func main() {
base := errors.New("数据库连接失败")
err := WrapError(500, "服务不可用", base)
fmt.Println(err)
fmt.Printf("原始错误: %v\n", errors.Unwrap(err))
}
3.2 错误码枚举
type ErrCode int
const (
ErrCodeOK ErrCode = 0
ErrCodeParam ErrCode = 400
ErrCodeUnauthorized ErrCode = 401
ErrCodeForbidden ErrCode = 403
ErrCodeNotFound ErrCode = 404
ErrCodeServer ErrCode = 500
)
func (c ErrCode) String() string {
switch c {
case ErrCodeOK:
return "成功"
case ErrCodeParam:
return "参数错误"
case ErrCodeUnauthorized:
return "未授权"
case ErrCodeForbidden:
return "禁止访问"
case ErrCodeNotFound:
return "资源未找到"
case ErrCodeServer:
return "服务器错误"
default:
return "未知错误"
}
}
type APIError struct {
Code ErrCode
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("%d %s: %s", e.Code, e.Code.String(), e.Message)
}
func (e *APIError) Unwrap() error {
return errors.New(e.Message)
}
四、错误处理最佳实践
4.1 错误处理原则
// 1. 及早处理错误
func badExample() error {
data, _ := os.ReadFile("test.txt") // 忽略错误(不好)
return nil
}
func goodExample() error {
data, err := os.ReadFile("test.txt")
if err != nil {
return fmt.Errorf("读取文件: %w", err)
}
// 处理数据
return nil
}
// 2. 不要忽略错误(除非明确意图)
func documentedIgnore() {
_, err := io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if err != nil {
// io.Copy到io.Discard,忽略写入错误是合理的
}
}
// 3. 错误应该包含上下文
func processData(data []byte) error {
err := validate(data)
if err != nil {
return fmt.Errorf("数据验证失败: %w", err) // 添加上下文
}
return nil
}
4.2 统一错误处理
type Handler func() error
// 统一处理错误的包装函数
func HandleError(handler Handler) {
if err := handler(); err != nil {
log.Printf("执行失败: %v\n", err)
// 统一错误处理逻辑
}
}
func main() {
HandleError(func() error {
return doSomething()
})
}
4.3 批量错误处理
import "errors"
type MultiError struct {
Errors []error
}
func (m *MultiError) Error() string {
if len(m.Errors) == 0 {
return ""
}
return fmt.Sprintf("%d errors occurred", len(m.Errors))
}
func (m *MultiError) Add(err error) {
if err != nil {
m.Errors = append(m.Errors, err)
}
}
func main() {
var multiErr MultiError
tasks := []func() error{
func() error { return nil },
func() error { return errors.New("任务1失败") },
func() error { return nil },
func() error { return errors.New("任务2失败") },
}
for _, task := range tasks {
multiErr.Add(task())
}
if len(multiErr.Errors) > 0 {
fmt.Printf("部分任务失败: %v\n", &multiErr)
for i, err := range multiErr.Errors {
fmt.Printf(" 错误%d: %v\n", i+1, err)
}
}
}
五、panic与recover
5.1 panic触发
func main() {
fmt.Println("开始")
panic("这是一个panic")
fmt.Println("永远不会执行")
}
输出:
开始
panic: 这是一个panic
goroutine 1 [running]:
main.main()
.../main.go:6
exit status 2
5.2 recover拦截panic
func safeExecute(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
f()
return nil
}
func main() {
err := safeExecute(func() {
panic("something went wrong")
})
if err != nil {
fmt.Printf("捕获错误: %v\n", err)
}
}
5.3 何时使用panic
合理使用panic的场景:
-
不可恢复的程序错误(如数组越界)
-
初始化失败(如配置文件缺失)
-
必须在编译时确定的错误
不应该使用panic的场景:
-
预期的错误(如文件不存在)→ 使用error
-
网络超时 → 使用error + 重试
-
用户输入错误 → 使用error
// 合理的panic
func NewConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取配置: %w", err) // 使用error
}
if len(data) == 0 {
panic("配置文件不能为空") // 不可恢复,使用panic
}
return parseConfig(data)
}
六、常见面试题
Q1: 错误和异常的区别
// 错误(Error):可预期的失败,应该被处理
// 异常(Panic):不可预期的失败,不应该被常规处理
// Go的哲学:错误是值,应该被显式处理
// 只有真正的不可恢复情况才使用panic
Q2: errors.Is和errors.As的区别
// errors.Is:检查错误链中是否有匹配的哨兵错误
errors.Is(err, ErrNotFound)
// errors.As:在错误链中找到第一个匹配类型的错误
var e *MyError
errors.As(err, &e)
Q3: 错误包装的三种方式
// 方式1:fmt.Errorf with %w
err := fmt.Errorf("包装: %w", originalErr)
// 方式2:自定义Error实现Unwrap()
type MyError struct { err error }
func (e *MyError) Unwrap() error { return e.err }
// 方式3:errors.Join (Go 1.20+)
err := errors.Join(err1, err2, err3)
总结
-
error接口:Error() string方法
-
创建错误:errors.New、fmt.Errorf、自定义类型
-
哨兵错误:预定义的错误值用于比较
-
错误包装:保留错误链,添加上下文
-
panic恢复:只用于真正不可恢复的情况
最佳实践:
-
尽早处理错误,不要忽略错误
-
使用fmt.Errorf添加上下文
-
定义有意义的错误类型
-
避免滥用panic
-
使用errors.Join处理多错误
💡 后续会继续更新Go语言其他知识点的系列文章!