目 录CONTENT

文章目录
go

【Go】错误处理

hxuanyu
2026-02-04 / 0 评论 / 0 点赞 / 18 阅读 / 0 字

错误处理

Go 的错误处理机制

Go 的错误处理风格与 C 一脉相承:函数通过返回值将错误显式交给调用方处理,从而迫使开发者在每个调用点都关注失败路径。不同于 C 常用“单一返回值 + 约定(如 -1/NULL)”来表示失败,Go 通过多返回值将“正常结果”和“错误”分离,通常将 error 放在返回值列表末尾,使成功路径与失败路径更清晰,也更容易形成统一的错误处理习惯。

Go 的惯用法是使用内置接口类型 error 表示错误:

func errorReturn() (int, error) {
	a := 2
	return a, errors.New("error")
}

错误值构造与检视

error 类型

error 是 Go 的内置接口类型,定义如下:

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
	Error() string
}

任何实现了 Error() string​ 方法的类型,都可以作为错误值赋给 error 接口。其优势包括:

  1. 统一错误抽象:提升可读性,便于形成一致的错误处理策略。
  2. 错误作为值传播​:可以进行 err == nil​ 判断;但是否能用 == 比较两个非 nil 错误,取决于其底层动态类型是否可比较(标准库常见错误值一般可比较,但并非所有自定义错误都可比较)。
  3. 易扩展:可通过自定义错误类型携带上下文信息(字段、方法等)。

构造错误

标准库提供了两种常用构造方式:

  • errors.New:创建一个仅包含字符串的错误
  • fmt.Errorf:创建带格式化信息的错误(并可用于错误包装)
func main() {
	err := errors.New("error")
	errorWithContext := fmt.Errorf("error with context: %s", "additional info")

	fmt.Printf("%T\n", err)              // *errors.errorString
	fmt.Println(err)                    // error
	fmt.Printf("%T\n", errorWithContext) // *errors.errorString(不使用 %w 时通常如此)
	fmt.Println(errorWithContext)        // error with context: additional info
}

两者在不使用 %w​ 的情况下,通常返回的都是 *errors.errorString

// errorString is a trivial implementation of error.
type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

当仅靠字符串不足以表达错误信息(例如需要错误码、发生时间、操作对象等)时,通常通过自定义错误类型扩展上下文:

type MyError struct {
	When    time.Time
	What    string
	Context string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("%s at %s: %s", e.What, e.When.Format(time.RFC3339Nano), e.Context)
}

构造错误链(错误包装)

实际工程中,错误往往跨越多层调用传播。中间层在增加上下文信息时,不应丢弃原始错误,否则会显著增加定位难度。Go 1.13 起,推荐使用错误包装(wrapping)构建错误链。

func bottomFunction(needError bool) (bool, error) {
	if needError {
		return needError, errors.New("error from bottomFunction")
	}
	return needError, nil
}

func middleFunction(needError bool) (bool, error) {
	result, err := bottomFunction(needError)
	if err != nil {
		return result, fmt.Errorf("middleFunction failed: %w", err)
	}
	return result, nil
}

错误链保存了每一层增加的上下文信息以及被包装的原始错误。标准库常用的组合方式有:

  • fmt.Errorf("...: %w", err):包装单个错误形成链
  • errors.Join(err1, err2, ...):将多个错误组合为一个(它不是“链式单根因”,而是“多错误聚合”)

下面示例通过 %w 构造单根因的错误链,并提取最底层根因:

package main

import (
	"errors"
	"fmt"
)

func rootCause(err error) error {
	for err != nil {
		u, ok := err.(interface{ Unwrap() error })
		if !ok {
			return err
		}
		err = u.Unwrap()
	}
	return nil
}

func main() {
	err1 := errors.New("error1") // 根因
	err2 := fmt.Errorf("error2: %w", err1)
	err3 := fmt.Errorf("error3: %w", err2)
	err := fmt.Errorf("error: %w", err3)

	fmt.Println(err)
	fmt.Println("root cause is", rootCause(err))
	fmt.Println(rootCause(err) == err1) // true
}

fmt.Errorf​ 的 %w只能包装一个错误​。如果需要组合多个错误,应使用 errors.Join

package main

import (
	"errors"
	"fmt"
)

func main() {
	err1 := errors.New("error1")
	err2 := errors.New("error2")
	err3 := errors.New("error3")

	err := errors.Join(err1, err2, err3)
	fmt.Println(err)
}

errors.Join​ 返回的错误实现了 Unwrap() []error,用于表示“包含多个子错误”。

检视错误值

最常见、最可靠的判断是 err == nil​。对于少数被定义为“哨兵错误”(sentinel error)的值(如 io.EOF​),可以直接用 == 比较:

if err == io.EOF {
	// 处理读到结尾
}

但对于“错误链/错误包装”,不要用 ==​ 去判断链上是否包含某个错误;应使用 errors.Is

package main

import (
	"errors"
	"fmt"
)

func main() {
	err1 := errors.New("error1")
	err2 := fmt.Errorf("error2: %w", err1)
	err3 := fmt.Errorf("error3: %w", err2)
	err := fmt.Errorf("error: %w", err3)

	fmt.Println(errors.Is(err, err1)) // true
	fmt.Println(errors.Is(err, err2)) // true
	fmt.Println(errors.Is(err, err3)) // true
}

若要按类型匹配(尤其是自定义错误类型、或标准库特定类型错误),使用 errors.As

package main

import (
	"errors"
	"fmt"
)

type MyError struct{ e string }

func (e *MyError) Error() string { return e.e }

func main() {
	var base error = &MyError{"MyError demo"}
	err1 := fmt.Errorf("wrap: %w", base)
	err2 := fmt.Errorf("wrap again: %w", err1)

	var target *MyError
	if errors.As(err2, &target) {
		fmt.Println("MyError is on the chain of err2")
		fmt.Println(target == base) // true
		return
	}
	fmt.Println("MyError is not on the chain of err2")
}

Go 1.13 及后续版本中,检视错误链时应优先使用 errors.Is / errors.As,而不是靠字符串匹配或手写类型断言遍历。

异常处理

在 Go 中,异常通过 panic 表达。

panic 简介

panic 表示程序运行期间出现了无法(或不打算)在当前层级继续处理的异常情况,主要来源包括:

  • 运行时触发(如数组越界、空指针解引用等)
  • 程序主动调用 panic(...)

当发生 panic 时(panicking 过程):

  1. 触发 panic​ 的函数立即停止执行,但该函数已注册的 defer 会按“后进先出”顺序执行。
  2. 执行完当前函数的 defer​ 后,控制权回到调用者;对调用者而言,相当于调用点发生了 panic​,继续展开调用栈并执行各层 defer
  3. 若整个 goroutine 的调用栈都展开完仍未恢复(recover),程序会打印堆栈并终止(默认会导致进程退出;即使是其他 goroutine 仍在运行,也会一起退出)。

示例:

package main

import "fmt"

func main() {
	foo()
}

func foo() {
	fmt.Println("foo")
	bar()
	fmt.Println("will not execute")
}

func bar() {
	fmt.Println("bar")
	panic("error")
	// zoo() // 不会执行
}

func zoo() {
	fmt.Println("zoo")
}

由于 defer​ 在 panic​ 时仍会执行,Go 提供了在 defer​ 中通过 recover() 捕获并恢复的机制:

func bar() {
	defer func() {
		if e := recover(); e != nil {
			fmt.Println("recover:", e)
		}
	}()
	fmt.Println("bar")
	panic("error")
}

recover​ 只能在同一 goroutinedefer​ 函数中生效;在普通代码中调用 recover​ 不会捕获到正在展开的 panic

应对 panic

使用 defer + recover 虽然可行,但滥用会增加复杂度。常见策略:

  • 评估系统对崩溃的容忍度​:命令行程序崩溃成本较低;服务端程序稳定性要求更高,通常在入口(如 HTTP middleware、goroutine worker 边界)统一 recover,避免单个请求/任务导致整个进程退出。
  • 用 panic 暴露潜在 bug(断言/不变量) ​:当出现“理论上不可能发生”的分支时,用 panic 让问题尽早暴露并携带堆栈,便于定位。
  • 不要把 panic 当作常规错误返回机制​:可预期的失败(文件不存在、超时、IO 错误、业务校验失败等)应返回 error​ 交给调用方处理。公共 API 更应避免向外泄露 panic​(除非文档明确说明,或提供 MustXxx 风格函数让调用方显式选择“失败即崩溃”)。

error 的典型场景

  • 外部依赖导致的失败:网络、磁盘、数据库、权限、超时、第三方接口返回错误
  • 输入不合法/业务校验失败(用户/请求造成)
  • 任何你希望调用方“有机会处理”的失败
  • 库代码几乎都应返回 error​,而不是 panic(除非明确写在文档里)

panic 的典型场景

  • 程序员错误:数组越界、nil 指针、必然成立的类型断言却失败、逻辑上不可能发生却发生
  • 内部不变量被破坏:例如约定“这里 map 一定有 key”,结果没有
  • 启动/初始化阶段不可继续错误:如必须配置缺失、模板编译失败(也可返回 error​ 并在 main​ 中 log.Fatal
  • 标准库风格的 Must​ 函数:如 template.Must(...),调用者明确选择“失败就崩”
0
博主关闭了所有页面的评论