在Go 语言中,错误处理是日常开发中必不可少的一环。Go 的哲学非常明确:错误是显式的值,而不是异常机制。这与 Java、Python 等语言通过 try/catch 捕获异常的方式截然不同。
标准库 errors 包是 Go 提供的基础错误处理工具,它不仅允许我们创建和包装错误,还支持丰富的功能,如错误链、类型判断、格式化输出等。本文将从基础到高级,带你深入掌握 errors 包的使用。
一、errors 包概述
Go 的 errors 包位于标准库,提供了以下核心功能:
errors.New():创建简单错误errors.Is():判断错误类型errors.As():提取特定类型错误errors.Unwrap():获取底层原始错误fmt.Errorf():创建带格式化信息和错误链的错误
自 Go 1.13 起,错误包装(error wrapping)成为官方推荐的方式,帮助构建可追踪的错误链。
二、创建基础错误
最简单的方式是使用 errors.New():
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("文件不存在")
if err != nil {
fmt.Println("错误:", err)
}
}
输出:
错误: 文件不存在
特点:
- 简单直接
- 无错误类型信息
- 适合小型项目或简单错误提示
三、使用 fmt.Errorf 创建格式化错误
fmt.Errorf() 可以创建带上下文的错误,并支持 错误链:
package main
import (
"errors"
"fmt"
)
func readFile(filename string) error {
return fmt.Errorf("读取文件 %s 失败: %w", filename, errors.New("文件不存在"))
}
func main() {
err := readFile("test.txt")
fmt.Println(err)
}
输出:
读取文件 test.txt 失败: 文件不存在
说明:
%w占位符用于 包装底层错误- 包装后的错误可以被
errors.Is()或errors.As()判断
四、判断错误类型:errors.Is
在复杂系统中,一个函数可能返回多种错误类型,需要判断某个错误是否属于特定类别。
示例:
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("资源未找到")
func findResource(id int) error {
return fmt.Errorf("查询 id=%d 出错: %w", id, ErrNotFound)
}
func main() {
err := findResource(1001)
if errors.Is(err, ErrNotFound) {
fmt.Println("处理未找到错误")
} else {
fmt.Println("其他错误:", err)
}
}
输出:
处理未找到错误
说明:
errors.Is会递归检查错误链- 可以判断底层错误类型,而无需解析错误字符串
五、提取特定类型错误:errors.As
在 Go 中,我们可以定义自定义错误类型:
package main
import (
"errors"
"fmt"
)
type MyError struct {
Code int
Msg string
}
func (e *MyError) Error() string {
return fmt.Sprintf("错误 %d: %s", e.Code, e.Msg)
}
func doSomething() error {
return &MyError{Code: 404, Msg: "资源不存在"}
}
func main() {
err := doSomething()
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Println("捕获到 MyError,Code =", myErr.Code)
} else {
fmt.Println("其他错误:", err)
}
}
输出:
捕获到 MyError,Code = 404
说明:
errors.As可以从错误链中提取特定类型的错误- 适合需要根据错误类型处理逻辑的场景
六、获取底层错误:errors.Unwrap
当错误被多次包装时,有时需要获取最原始的错误:
package main
import (
"errors"
"fmt"
)
func main() {
baseErr := errors.New("数据库连接失败")
err1 := fmt.Errorf("初始化服务失败: %w", baseErr)
err2 := fmt.Errorf("启动应用失败: %w", err1)
fmt.Println("完整错误:", err2)
fmt.Println("底层错误:", errors.Unwrap(errors.Unwrap(err2)))
}
输出:
完整错误: 启动应用失败: 初始化服务失败: 数据库连接失败
底层错误: 数据库连接失败
七、结合 HTTP 请求示例
在 Web 服务中,错误处理尤为关键:
package main
import (
"errors"
"fmt"
"net/http"
)
var ErrUnauthorized = errors.New("未授权")
func auth(req *http.Request) error {
token := req.Header.Get("Authorization")
if token != "123456" {
return fmt.Errorf("认证失败: %w", ErrUnauthorized)
}
return nil
}
func handler(w http.ResponseWriter, r *http.Request) {
err := auth(r)
if errors.Is(err, ErrUnauthorized) {
http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
return
}
if err != nil {
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
return
}
fmt.Fprintln(w, "访问成功")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
说明:
- 错误包装提供上下文信息
errors.Is判断类型- 支持可维护的错误链
八、自定义错误类型进阶
除了简单字符串错误,可以创建结构化错误:
type DBError struct {
Query string
Err error
}
func (e *DBError) Error() string {
return fmt.Sprintf("查询失败 [%s]: %v", e.Query, e.Err)
}
func (e *DBError) Unwrap() error {
return e.Err
}
特点:
- 可以携带上下文信息
- 仍然支持
errors.Is和errors.As - 可扩展为日志、监控使用
九、错误链与日志
使用错误链,配合日志框架,可以实现可追踪的异常记录:
err := fmt.Errorf("处理订单失败: %w", dbErr)
log.Printf("%+v", err)
在第三方库如 pkg/errors 或 Go 1.20+ errors 配合 fmt %+v 可以打印完整错误链,便于排查问题。
十、常见误区
- 直接比较错误字符串
if err.Error() == "数据库连接失败" {}
- 错误:不可靠
- 正确:使用
errors.Is()
- 忽略取消和超时错误
在使用 context、网络或数据库时,注意捕获 context.Canceled 和 context.DeadlineExceeded。
- 滥用 Value
错误信息不要放到 context.Value 中,context 只适合传递 trace 或元信息。
十一、实战建议
- 使用
errors.New创建基础错误 - 使用
fmt.Errorf("%w")包装错误 - 使用
errors.Is判断类型 - 使用
errors.As提取结构化错误 - 保持错误链,便于日志追踪
- 提供上下文信息而不是简单错误字符串
十二、总结
Go 的 errors 包虽然简单,但提供了强大的错误处理机制:
- 显式、可控、可追踪
- 支持错误链和类型判断
- 与标准库和 context 深度结合
在现代 Go 项目中,掌握 errors 的使用技巧至关重要,无论是 Web 服务、微服务还是 CLI 工具,都能大幅提升代码健壮性和可维护性。