






















错误处理是 Go 语言设计哲学的重要体现。与许多语言使用异常(exception)不同,Go 将错误作为普通的返回值显式处理,使错误流清晰可见。本章系统讲解 Go 的错误处理机制,包括 error 接口、错误包装、panic 与 recover。
Go 的错误以 error 接口表示,它是内置接口,定义极其简单:
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都是一个 error。
Go 的惯用模式是:函数将 error 作为最后一个返回值,调用者立即检查:
func readConfig(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err // 向上传播错误
}
return string(data), nil
}
// 调用方
content, err := readConfig("config.yaml")
if err != nil {
log.Fatal(err) // 处理错误
}
fmt.Println(content)
这种 if err != nil 的检查在 Go 代码中无处不在。虽然略显冗长,但它强制开发者正视每一个可能的错误,避免错误被悄悄忽略。
创建简单的字符串错误:
import "errors"
var ErrNotFound = errors.New("记录未找到")
func find(id int) error {
if id < 0 {
return errors.New("id 不能为负数")
}
return nil
}
需要格式化错误信息时使用:
func validate(age int) error {
if age < 0 || age > 150 {
return fmt.Errorf("年龄 %d 不在合法范围内", age)
}
return nil
}
预先定义的、可供比较的错误变量称为哨兵错误,常用于表示特定的错误状态:
var (
ErrNotFound = errors.New("not found")
ErrPermission = errors.New("permission denied")
)
if err == ErrNotFound {
// 特殊处理
}
标准库中的 io.EOF、sql.ErrNoRows 都是典型的哨兵错误。
当需要携带更多上下文信息时,可定义实现了 error 接口的结构体:
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("字段 %s(值 %v)校验失败: %s", e.Field, e.Value, e.Message)
}
func validateName(name string) error {
if name == "" {
return &ValidationError{
Field: "name",
Value: name,
Message: "不能为空",
}
}
return nil
}
调用者可以通过类型断言提取详细信息:
err := validateName("")
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("出错字段:", ve.Field)
}
Go 1.13 引入了错误包装机制,可以在传播错误时附加上下文,同时保留原始错误,形成错误链。
在 fmt.Errorf 中使用 %w 动词包装一个错误:
func loadUser(id int) error {
err := queryDB(id)
if err != nil {
return fmt.Errorf("加载用户 %d 失败: %w", id, err)
}
return nil
}
err := loadUser(1)
if errors.Is(err, sql.ErrNoRows) {
// 即使 err 被多层包装,也能正确判断
fmt.Println("用户不存在")
}
errors.Is 会沿着错误链逐层解包比较,替代了直接用 == 比较的局限。
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("校验错误字段:", ve.Field)
}
wrapped := fmt.Errorf("外层: %w", ErrNotFound)
inner := errors.Unwrap(wrapped) // 返回 ErrNotFound
最佳实践:在跨越函数/模块边界传播错误时,使用
%w添加上下文;在需要根据错误类型决策时,使用errors.Is和errors.As,而非直接比较或类型断言。
panic 和 recover 是 Go 处理不可恢复的、程序级异常的机制,但它们不应用于常规的错误处理。
panic 会立即中断当前函数的正常执行流程,开始向上回溯(unwind)调用栈,执行沿途的 defer,最终若无人捕获则导致程序崩溃并打印堆栈信息:
func mustPositive(n int) {
if n < 0 {
panic("数值不能为负")
}
}
触发 panic 的常见情况还包括:数组越界、空指针解引用、向 nil map 写入、类型断言失败等。
recover 只能在 defer 函数中调用,用于捕获 panic、恢复程序的正常执行:
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("发生 panic: %v", r)
}
}()
return a / b, nil // 当 b 为 0 时触发 panic
}
result, err := safeDivide(10, 0)
fmt.Println(result, err) // 0 发生 panic: runtime error: integer divide by zero
这里利用了命名返回值 err,在 defer 中将 panic 转换为普通错误返回。
应当极少使用 panic。合理的使用场景包括:
原则:对于可预期的、调用者应当处理的错误,返回 error;对于不可恢复的、表明程序进入错误状态的情况,才使用 panic。
_ 丢弃 error,除非你明确知道可以忽略。%w 说明"在做什么时出错",便于排查。"open file failed" 而非 "Open file failed."。== 比较或类型断言。本章详细讲解了 Go 独特的错误处理哲学:将错误作为显式返回值处理,而非依赖异常。我们学习了 error 接口、哨兵错误、自定义错误类型,以及 Go 1.13 引入的错误包装机制(%w、errors.Is、errors.As)。panic/recover 则用于处理真正不可恢复的异常情况,应谨慎使用。良好的错误处理是编写健壮 Go 程序的核心能力。
下一章我们将进入 Go 的招牌特性——并发编程,学习 goroutine 与 channel。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。