Go 语言错误处理
Go 语言采用了与其他主流语言截然不同的错误处理方式:显式处理,不依赖异常。错误是普通的值,通过返回值传递,由调用者显式检查和处理。本篇将系统介绍 Go 的错误处理机制,从基础的 error 接口到高级的错误包装与判断。
Go 的错误处理哲学
Go 认为错误是程序执行过程中正常可能出现的情况,不应该用异常来控制程序流程。典型的模式是:
result, err := doSomething()
if err != nil {
return err
}
// 使用 result这种模式的优势:错误处理逻辑紧跟调用点、不会跳过中间代码、强制开发者思考每个可能出错的地方。
error 接口
Go 内置的 error 接口只有一个方法:Error() string。任何实现了该方法的类型都是合法的错误类型,函数通常将 error 作为最后一个返回值。
创建错误
errors.New
最简单的方式是使用 errors.New 创建一个包含固定消息的错误:
package main
import (
"errors"
"fmt"
)
func validateAge(age int) error {
if age < 0 {
return errors.New("年龄不能为负数")
}
if age > 150 {
return errors.New("年龄不合理")
}
return nil
}
func main() {
err := validateAge(-1)
if err != nil {
fmt.Println("错误:", err) // 错误: 年龄不能为负数
}
}fmt.Errorf
当需要在错误消息中包含动态信息时,使用 fmt.Errorf:
package main
import "fmt"
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为零 (a=%v, b=%v)", a, b)
}
return a / b, nil
}
func main() {
_, err := divide(10, 0)
if err != nil {
fmt.Println("错误:", err) // 错误: 除数不能为零 (a=10, b=0)
}
}自定义错误类型
当需要携带更多信息或区分不同错误类型时,可以定义自己的错误类型:
package main
import (
"fmt"
"time"
)
type TimeoutError struct {
Duration time.Duration
Message string
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("超时: %v 后 %s", e.Duration, e.Message)
}
func fetchData(timeout time.Duration) error {
return &TimeoutError{Duration: timeout, Message: "等待服务器响应"}
}
func main() {
err := fetchData(5 * time.Second)
if err != nil {
fmt.Println(err) // 超时: 5s 后 等待服务器响应
}
}错误判断:errors.Is
errors.Is 用于判断错误链中是否包含某个特定的目标错误,类似于 == 但能处理错误包装:
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("未找到")
func findUser(id int) error {
if id <= 0 {
return fmt.Errorf("查询用户 %d: %w", id, ErrNotFound)
}
return nil
}
func main() {
err := findUser(-1)
if errors.Is(err, ErrNotFound) {
fmt.Println("用户不存在") // 输出: 用户不存在
}
}对比:旧写法
err == ErrNotFound无法穿透包装的错误,errors.Is解决了这个问题。
错误包装:fmt.Errorf + %w
Go 1.13 引入了 %w 动词,可以在错误消息中包装另一个错误,形成错误链:
package main
import (
"errors"
"fmt"
)
var ErrDatabase = errors.New("数据库错误")
func queryUser(id int) error {
if id == 0 {
return fmt.Errorf("查询用户失败: %w", ErrDatabase)
}
return nil
}
func getUser(id int) error {
err := queryUser(id)
if err != nil {
return fmt.Errorf("获取用户信息失败: %w", err)
}
return nil
}
func main() {
err := getUser(0)
fmt.Println("错误:", err)
// 输出: 错误: 获取用户信息失败: 查询用户失败: 数据库错误
// errors.Is 可以穿透错误链
fmt.Println("是数据库错误?", errors.Is(err, ErrDatabase)) // true
}注意:
%w只能包装error类型的值。如果只需格式化不包装,用%v或%s。
错误解包:errors.As
errors.As 用于从错误链中提取特定类型的错误,类似于类型断言但能穿透包装:
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("验证失败 [%s]: %s", e.Field, e.Message)
}
func createUser(name string, age int) error {
if age < 0 || age > 150 {
return fmt.Errorf("创建用户失败: %w", &ValidationError{
Field: "age", Message: fmt.Sprintf("无效年龄: %d", age),
})
}
return nil
}
func main() {
err := createUser("张三", -5)
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("字段 %s 验证失败: %s\n", valErr.Field, valErr.Message)
// 输出: 字段 age 验证失败: 无效年龄: -5
}
}panic 和 recover
Go 提供了 panic 和 recover 机制来处理不可恢复的严重错误,但日常开发中应极少使用。
panic 的使用场景
panic 用于表示程序无法继续执行的严重错误,例如:程序启动时关键配置缺失、不可能到达的代码路径等。
原则:如果错误可以被调用者合理处理,就返回
error;只有当错误意味着程序不应该继续运行时,才使用panic。
recover 的正确用法
recover 用于捕获 panic,必须在 defer 函数中调用才有效:
package main
import "fmt"
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("发生了 panic: %v", r)
}
}()
if b == 0 {
panic("除零错误")
}
return a / b, nil
}
func main() {
result, err := safeDiv(10, 0)
if err != nil {
fmt.Println("捕获到错误:", err)
} else {
fmt.Println("结果:", result)
}
}示例:用 recover 实现安全的 HTTP Handler
在生产环境中,可以用 recover 防止单个请求的 panic 导致整个服务崩溃:
package main
import (
"log"
"net/http"
)
// safeHandler 包装 handler,捕获 panic 防止服务崩溃
func safeHandler(handler func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("捕获到 panic: %v, 请求: %s %s", rec, r.Method, r.URL.Path)
http.Error(w, "服务器内部错误", http.StatusInternalServerError)
}
}()
handler(w, r)
}
}
func main() {
// 使用 safeHandler 包装所有 handler
http.HandleFunc("/", safeHandler(func(w http.ResponseWriter, r *http.Request) {
// 业务逻辑...
w.Write([]byte("OK"))
}))
log.Fatal(http.ListenAndServe(":8080", nil))
}错误处理最佳实践
1. 尽早检查错误,在源头添加上下文
错误处理应紧跟调用点,并在包装时添加上下文信息:
package main
import (
"fmt"
"os"
)
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
// 尽早检查,并添加上下文
return nil, fmt.Errorf("读取配置文件 %s 失败: %w", path, err)
}
return data, nil
}
func main() {
_, err := readConfig("/etc/myapp/config.yaml")
if err != nil {
fmt.Println(err)
}
}2. 不要忽略错误
// 不好的做法:忽略错误
result, _ := doSomething()
// 好的做法:至少记录错误
result, err := doSomething()
if err != nil {
log.Printf("警告: 操作失败: %v", err)
}3. 只处理一次错误,在边界层统一记录
不要在中间层重复记录错误日志。建议在边界层(如 main 函数、HTTP handler)统一记录,中间层只负责包装和传递:
// 中间层:只包装,不记录
func process() error {
err := doStep1()
if err != nil {
return fmt.Errorf("step1: %w", err)
}
return nil
}
// 边界层:统一记录
func main() {
if err := process(); err != nil {
log.Println(err)
}
}4. 定义哨兵错误
对于常见的错误类型,使用包级别的哨兵错误变量:
// 定义哨兵错误
var (
ErrEmptyInput = errors.New("输入为空")
ErrInvalidFormat = errors.New("格式无效")
)
func process(input string) error {
if input == "" {
return fmt.Errorf("处理数据: %w", ErrEmptyInput)
}
return nil
}
// 调用方用 errors.Is 判断
err := process("")
if errors.Is(err, ErrEmptyInput) {
fmt.Println("请提供输入数据")
}总结
本篇系统介绍了 Go 的错误处理机制。Go 通过显式的 error 返回值而非异常来处理错误,让程序流程更加清晰。errors.Is 和 errors.As 配合 %w 错误包装,构成了完整的错误链处理能力。panic/recover 仅用于不可恢复的严重场景。遵循"尽早检查、添加上下文、在边界层统一处理"等最佳实践,可以写出健壮且可维护的错误处理代码。