前言

Go 没有 try/catch,错误是返回值,必须逐个处理。初看啰嗦,实则强迫你思考每一个失败路径。这篇把现代 Go(1.13+)的错误处理体系讲全。

一、error 是什么

// 标准库定义: 就一个接口
type error interface {
	Error() string
}
// 最常见的形态
func ReadConfig(path string) ([]byte, error) {
	return os.ReadFile(path)      // os.ReadFile 的签名: ( []byte, error)
}

func main() {
	data, err := ReadConfig("app.conf")
	if err != nil {
		log.Fatal(err)            // open app.conf: no such file or directory
	}
	fmt.Println(len(data))
}

铁律:err != nil 检查永远紧跟在产生它的调用后;不要用 _ 吞掉错误(除非明确说明为何可忽略)。

二、创建错误与错误包装

import (
	"errors"
	"fmt"
)

// 创建
err1 := errors.New("disk full")
err2 := fmt.Errorf("port %d 非法", 808000)      // 带格式化

// 包装(wrap): 加上下文, 同时保留根因 —— %w 是关键!
if err := connect(db); err != nil {
	return fmt.Errorf("init db: %w", err)
}
// 输出: init db: dial tcp 10.0.0.2:3306: i/o timeout
// 内部仍可透传判断出根因

%v vs %w:%v 只是拼接文字(丢失类型信息);%w 形成错误链,允许 errors.Is/As 沿链查找。

三、errors.Is / As / Join

// sentinel 错误: 预定义的标志性错误值
var ErrNotFound = errors.New("not found")

func GetUser(id int) error {
	// ... 查库失败时: return fmt.Errorf("get user %d: %w", id, ErrNotFound)
}

if err := GetUser(42); err != nil {
	if errors.Is(err, ErrNotFound) {       // 沿错误链找相同值
		// 处理"不存在": 返回 404 等
	}
	// 其他错误继续上抛
}
// 判断具体类型(结构化错误)
type TimeoutError struct {
	Op  string
	Dur time.Duration
}
func (e *TimeoutError) Error() string {
	return fmt.Sprintf("%s timed out after %s", e.Op, e.Dur)
}

var te *TimeoutError
if errors.As(err, &te) {                    // 沿链找可赋值给 *TimeoutError 的
	log.Printf("操作 %s 超时, 时长 %s", te.Op, te.Dur)
}
// 合并多个错误(并行任务汇总场景, go1.20+)
var errs []error
for _, u := range urls {
	errs = append(errs, check(u))
}
if err := errors.Join(errs...); err != nil {   // nil 会被跳过
	return err                                  // 多行错误信息一次输出
}

四、自定义错误类型

// 好的错误类型: 携带结构化上下文
type HTTPError struct {
	StatusCode int
	URL        string
	Err        error            // 内层错误
}

func (e *HTTPError) Error() string {
	return fmt.Sprintf("http %s: %d: %v", e.URL, e.StatusCode, e.Err)
}

func (e *HTTPError) Unwrap() error {      // 支持 errors.Is/As 沿链下钻
	return e.Err
}

func Fetch(url string) ([]byte, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, &HTTPError{URL: url, Err: err}       // 500? 不, 网络层错
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return nil, &HTTPError{URL: url, StatusCode: resp.StatusCode}
	}
	return io.ReadAll(resp.Body)
}

// 调用方精确处理
var he *HTTPError
if errors.As(err, &he) && he.StatusCode == 429 {
	time.Sleep(time.Second)       // 限流: 退避重试
}

五、panic / recover:留给真正的异常

Go 中 error 是业务的一部分,panic 是程序员的 bug:

func mustParse(cfg string) Config {
	c, err := parse(cfg)
	if err != nil {
		panic(fmt.Sprintf("配置无法解析: %v", err))   // 程序启动期的不变量被破坏
	}
	return c
}

// recover: 只在 defer 中生效, 用于兜底防止进程崩溃
func safeCall() (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("panic recovered: %v", r)
		}
	}()
	panic("boom")
}

什么时候用 panic:

  • ✅ 程序初始化阶段的致命错误(配置错误、依赖缺失)——快速失败
  • ✅ 库内部不变量被破坏(不可能的分支)
  • ❌ 日常业务错误(文件不存在、网络超时)——用 error
  • ❌ 通过 panic 跨层传业务错误——性能差且语义混乱

六、工程化惯例

// 1. 包级 sentinel 错误集中定义(Err 前缀)
package store

var (
	ErrNotFound  = errors.New("not found")
	ErrConflict  = errors.New("conflict")
)

// 2. 返回错误时永远 wrap 上下文
if err := s.db.Query(...); err != nil {
	return fmt.Errorf("get user %d: %w", id, err)
}

// 3. 最外层统一记录(带堆栈)
log.Printf("handle error: %+v", err)      // %+v 打印详细信息

// 4. 处理不了的错误, 原样返回, 不要重复包装

错误信息规范:小写开头、不以标点结尾、包含关键参数(get user 42: connection refused)。

七、与 Python 异常的对比

维度 Go error Python exception
传递方式 返回值,显式逐层处理 抛出,隐式向上冒泡
忽略风险 _ = err 显式写出来 裸 except 静默吞掉
分类判断 errors.Is/As 沿链查 except 具体类型
崩溃机制 panic(仅 bug 级) 未捕获异常即崩溃
样板代码 多(if err != nil) 少

啰嗦是特性不是缺陷——每个错误都在你眼前过一遍。

小结

工具 用途
fmt.Errorf("...: %w", err) 包装 + 保留根因
errors.Is 判断链上是否有 sentinel
errors.As 提取链上特定类型
errors.Join 汇总多个错误
自定义类型 + Unwrap 结构化错误
panic/recover 只留给 bug 与初始化失败

本文是「Golang」系列第 6 篇。