Go 错误处理
错误处理是 Go 语言编程的重要组成部分。Go 采用了一种独特的错误处理方式:将错误作为普通值返回,而不是使用异常机制。这种设计使得错误处理更加显式和可控。
Go 的错误处理哲学
为什么不用异常?
Go 选择不使用异常机制,而是采用返回值的方式处理错误,原因如下:
- 显式性:错误处理代码就在业务代码旁边,不会被忽略
- 可控性:调用者决定如何处理错误,而不是被强制捕获
- 简单性:没有 try-catch-finally 的复杂控制流
- 可读性:代码流程清晰,易于理解
核心原则
Go 的错误处理遵循以下原则:
- 错误是值,可以像其他值一样传递和处理
- 如果可能发生错误,应该显式检查
- 错误信息应该提供足够的上下文
error 接口
Go 的错误处理基于 error 接口:
type error interface {
Error() string
}
解释:任何实现了 Error() string 方法的类型都可以作为错误使用。这个简单的接口设计使得错误处理非常灵活。
nil 表示成功
在 Go 中,约定俗成地使用 nil 表示没有错误:
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
result, err := divide(10, 2)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("结果:", result)
解释:函数返回两个值,第一个是结果,第二个是错误。如果 err == nil,表示操作成功。
创建错误
errors.New
最简单的创建错误的方式:
import "errors"
func validateAge(age int) error {
if age < 0 {
return errors.New("年龄不能为负数")
}
if age > 150 {
return errors.New("年龄不能超过150")
}
return nil
}
解释:errors.New 创建一个简单的错误,包含给定的错误信息。
fmt.Errorf
格式化错误信息:
import "fmt"
func fetchUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("无效的用户ID: %d", id)
}
return &User{ID: id}, nil
}
func openFile(path string) error {
_, err := os.Open(path)
if err != nil {
return fmt.Errorf("打开文件 %s 失败: %w", path, err)
}
return nil
}
解释:fmt.Errorf 支持格式化字符串,可以创建更具描述性的错误信息。使用 %w 动词可以包装其他错误(Go 1.13+)。
自定义错误类型
当需要携带更多错误信息时,可以定义自己的错误类型:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("验证错误 [%s]: %s", e.Field, e.Message)
}
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s (ID: %d) 不存在", e.Resource, e.ID)
}
func getUser(id int) (*User, error) {
if id <= 0 {
return nil, &ValidationError{
Field: "id",
Message: "必须为正整数",
}
}
user := findUser(id)
if user == nil {
return nil, &NotFoundError{
Resource: "User",
ID: id,
}
}
return user, nil
}
解释:自定义错误类型可以携带更多上下文信息,便于调用者进行错误处理。
错误处理模式
基本模式
result, err := someFunction()
if err != nil {
return err
}
错误检查优先
func processFile(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("打开文件失败: %w", err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("读取文件失败: %w", err)
}
if err := processData(data); err != nil {
return fmt.Errorf("处理数据失败: %w", err)
}
return nil
}
解释:这种模式先检查错误,再处理正常逻辑,使代码流程更加清晰。
立即返回
func doWork() error {
if err := step1(); err != nil {
return fmt.Errorf("步骤1失败: %w", err)
}
if err := step2(); err != nil {
return fmt.Errorf("步骤2失败: %w", err)
}
if err := step3(); err != nil {
return fmt.Errorf("步骤3失败: %w", err)
}
return nil
}
解释:每个步骤出错后立即返回,避免深层嵌套。
忽略错误
某些情况下可以忽略错误,但要确保这是有意为之:
// 写入文件,忽略错误(例如写入临时文件)
_ = os.WriteFile("/tmp/cache", data, 0644)
// 删除文件,忽略"文件不存在"的错误
_ = os.Remove(path)
注意:忽略错误应该是有意为之,并添加注释说明原因。
错误包装与解包
Go 1.13 引入了错误包装机制,允许在错误上添加上下文信息。
包装错误
使用 %w 包装错误:
func readConfig(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
defer file.Close()
return nil
}
解释:%w 会保留原始错误,使其可以被 errors.Is 和 errors.As 检测。
errors.Is
检查错误链中是否包含特定错误:
import "errors"
err := readConfig("config.json")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println("配置文件不存在,使用默认配置")
} else if errors.Is(err, os.ErrPermission) {
fmt.Println("没有权限读取配置文件")
} else {
fmt.Println("读取配置文件失败:", err)
}
}
解释:errors.Is 会遍历错误链,检查是否包含指定的错误值。
errors.As
从错误链中提取特定类型的错误:
import "errors"
err := getUser(-1)
if err != nil {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
fmt.Printf("验证失败: 字段 %s - %s\n", validationErr.Field, validationErr.Message)
return
}
var notFoundErr *NotFoundError
if errors.As(err, ¬FoundErr) {
fmt.Printf("资源未找到: %s (ID: %d)\n", notFoundErr.Resource, notFoundErr.ID)
return
}
fmt.Println("其他错误:", err)
}
解释:errors.As 会遍历错误链,尝试将错误转换为指定类型。
Unwrap
手动解包错误:
err := readConfig("config.json")
for err != nil {
fmt.Println("错误:", err)
err = errors.Unwrap(err)
}
自定义错误类型最佳实践
实现 Unwrap 方法
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("查询错误: %s: %v", e.Query, e.Err)
}
func (e *QueryError) Unwrap() error {
return e.Err
}
解释:实现 Unwrap() 方法使得错误可以被 errors.Is 和 errors.As 正确处理。
实现多个接口
type TimeoutError struct {
Op string
Err error
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("%s 超时: %v", e.Op, e.Err)
}
func (e *TimeoutError) Unwrap() error {
return e.Err
}
func (e *TimeoutError) Timeout() bool {
return true
}
func (e *TimeoutError) Temporary() bool {
return true
}
解释:实现 Timeout() 和 Temporary() 方法可以与 net.Error 接口配合使用。
Panic 和 Recover
Panic 和 Recover 是 Go 的异常机制,但应该谨慎使用。
Panic
Panic 会立即停止当前函数的执行,并开始展开调用栈:
func mustPositive(n int) {
if n < 0 {
panic(fmt.Sprintf("负数不被允许: %d", n))
}
}
适用场景:
- 程序遇到无法恢复的错误
- 初始化失败
- 不可能发生的情况(防御性编程)
Recover
Recover 用于捕获 panic,只能在 defer 函数中使用:
func safeOperation() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic 恢复: %v", r)
}
}()
mightPanic()
return nil
}
HTTP 服务器示例
func handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("处理请求时发生 panic: %v", err)
http.Error(w, "内部服务器错误", http.StatusInternalServerError)
}
}()
processRequest(r)
}
何时使用 Panic
应该使用 Panic:
- 程序初始化失败
- 达到不可能的代码路径
- 关键资源不可用
不应该使用 Panic:
- 文件不存在
- 网络连接失败
- 用户输入错误
- 配置错误
错误处理最佳实践
1. 提供足够的上下文
func loadConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("加载配置文件 %s 失败: %w", path, err)
}
return nil
}
2. 使用哨兵错误
var (
ErrNotFound = errors.New("资源未找到")
ErrUnauthorized = errors.New("未授权")
ErrInvalidInput = errors.New("无效输入")
)
func getItem(id int) (*Item, error) {
if id <= 0 {
return nil, ErrInvalidInput
}
item := findItem(id)
if item == nil {
return nil, ErrNotFound
}
return item, nil
}
func main() {
item, err := getItem(-1)
if errors.Is(err, ErrInvalidInput) {
fmt.Println("请提供有效的ID")
}
}
3. 错误日志记录
func processOrder(orderID int) error {
order, err := getOrder(orderID)
if err != nil {
log.Printf("获取订单失败 [orderID=%d]: %v", orderID, err)
return fmt.Errorf("处理订单失败: %w", err)
}
if err := validateOrder(order); err != nil {
log.Printf("验证订单失败 [orderID=%d]: %v", orderID, err)
return fmt.Errorf("订单验证失败: %w", err)
}
return nil
}
4. 分层错误处理
type ServiceError struct {
Code string
Message string
Err error
}
func (e *ServiceError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *ServiceError) Unwrap() error {
return e.Err
}
var (
ErrUserNotFound = &ServiceError{Code: "USER_001", Message: "用户不存在"}
ErrInvalidEmail = &ServiceError{Code: "USER_002", Message: "无效的邮箱地址"}
)
5. 使用 errors 包的函数
func main() {
err := doSomething()
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在")
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("路径错误: %s\n", pathErr.Path)
}
}
实战示例:数据库操作
package main
import (
"database/sql"
"errors"
"fmt"
"log"
)
var (
ErrUserNotFound = errors.New("用户不存在")
ErrDuplicateEmail = errors.New("邮箱已被使用")
)
type User struct {
ID int
Name string
Email string
}
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) FindByID(id int) (*User, error) {
query := "SELECT id, name, email FROM users WHERE id = ?"
var user User
err := r.db.QueryRow(query, id).Scan(&user.ID, &user.Name, &user.Email)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("查找用户失败 [id=%d]: %w", id, ErrUserNotFound)
}
if err != nil {
return nil, fmt.Errorf("查询数据库失败: %w", err)
}
return &user, nil
}
func (r *UserRepository) Create(user *User) error {
query := "INSERT INTO users (name, email) VALUES (?, ?)"
result, err := r.db.Exec(query, user.Name, user.Email)
if err != nil {
return fmt.Errorf("创建用户失败: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("获取用户ID失败: %w", err)
}
user.ID = int(id)
return nil
}
type UserService struct {
repo *UserRepository
}
func (s *UserService) GetUser(id int) (*User, error) {
user, err := s.repo.FindByID(id)
if err != nil {
return nil, fmt.Errorf("获取用户失败: %w", err)
}
return user, nil
}
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
repo := &UserRepository{db: db}
service := &UserService{repo: repo}
user, err := service.GetUser(1)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
fmt.Println("用户不存在")
} else {
log.Printf("错误: %v", err)
}
return
}
fmt.Printf("用户: %+v\n", user)
}
小结
- 错误是值:Go 将错误作为普通值处理,而不是异常
- 显式检查:每个可能出错的操作都应该检查错误
- 错误包装:使用
%w包装错误,保留错误链 - errors.Is/As:用于检查和提取错误链中的特定错误
- 自定义错误:实现
Error()和Unwrap()方法 - Panic/Recover:仅用于真正不可恢复的情况
- 提供上下文:错误信息应该包含足够的调试信息
- 分层处理:不同层级使用不同的错误类型
练习
- 实现一个自定义错误类型,包含错误码、错误消息和原始错误
- 编写一个函数,演示错误包装和
errors.Is的使用 - 实现一个 HTTP 客户端,正确处理各种网络错误
- 使用 panic/recover 实现一个安全的函数执行器
- 编写一个文件处理程序,演示分层错误处理