LabHub
学习 学习路径 课程

用 Go 写服务器

if err != nil 让人烦的原因

在 LabHub 中继续学习

一句话总结

Go 把错误作为值返回。因此无法轻易跳过错误处理,代价是代码变长;这种长度不是负担,而是显式性的价格

概念图: 作为值返回 · 显式性的价格 · 保留在内部 · 包装时始终使用 %w。

为什么需要它

在使用异常的语言中,仅看代码无法知道哪一行可能抛出异常,必须沿调用图追踪才能判断“这里会不会失败”。

Go 把它写进 signature。

func ReadConfig(path string) (*Config, error)

函数可能失败这一事实包含在类型中。调用方不能无声忽略它;要忽略就必须明确写 _,意图会显现出来。

包装错误

直接向上传递错误会丢失“在哪里发生”的信息,应使用 %w 包装。

if err != nil {
    return fmt.Errorf("설정 읽기(%s): %w", path, err)
}

%w 会把原始错误保留在内部,因此上层可以用 errors.Is / errors.As 询问原因。

if errors.Is(err, os.ErrNotExist) { ... }

var perr *os.PathError
if errors.As(err, &perr) { fmt.Println(perr.Path) }

使用 %v 包装只会留下字符串,无法再做上述判断。包装时始终使用 %w

什么时候使用 panic

几乎不用。panic 只适用于“程序已经没有继续运行的理由”,例如初始化失败或 invariant 被破坏。处理请求时发生 panic 应只终止该请求,因此 HTTP server 默认会捕获它。

常见误解

**“记录错误后再返回它。”**这样同一事故会在日志中出现多次。**要么处理,要么向上传递,只选一个。**日志只在最终处理处记录一次。

**“error 为 nil,结果就一定有效。”**按 Go 惯例通常如此,但语言并不强制;某些 library 可能允许两者都有效或都为 nil,应查看文档。

sentinel error 与自定义类型如何选择

取决于调用者需要知道什么。

**只需知道“是不是这种情况”时用 sentinel。**在 package 级声明一个值,用 errors.Is 比较。

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

if errors.Is(err, ErrNotFound) { ... }

**需要附加信息时用类型。**某个字段为何错误等信息必须放入值中。

type ValidationError struct {
    Field  string
    Reason string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Reason)
}

var verr *ValidationError
if errors.As(err, &verr) {
    log.Printf("필드 %s 가 문제: %s", verr.Field, verr.Reason)
}

有一个陷阱:把 nil 自定义错误装入 error interface 后,err != nil 会为真。

func do() error {
    var e *ValidationError    // nil
    ...
    return e                  // ❌ 인터페이스는 (타입, nil) 이라 nil 이 아니다
}
if do() != nil { /* 오류가 없는데 여기로 들어온다 */ }

不要直接返回具体类型变量;没有错误时应明确 return nil。这是 Go 最古老、至今仍会出现的陷阱之一。

一次处理多个错误

Go 1.20 起可用 errors.Join 合并多个错误,适合 validation 等“收集全部后一次告知”的场景。

var errs error
for _, f := range fields {
    if err := f.Validate(); err != nil {
        errs = errors.Join(errs, err)
    }
}
return errs      // 하나도 없으면 nil 이다

fmt.Errorf 也可以接受多个 %werrors.Is 会遍历所有被合并的错误。

每一层应添加什么

包装错误时,只添加自己知道的内容。重复下层已经说明的信息,只会让消息越来越长。

应添加 不应添加
repository table、key 完整 SQL(只写入日志)
service 原本要执行什么 repository 细节
handler request identifier 内部结构

并且要区分发给用户的信息与写入日志的信息。内部路径原样出现在 HTTP response 中,就是信息泄露。

if err != nil {
    log.Printf("주문 조회 실패: %v", err)          // 전체 사슬
    http.Error(w, "일시적인 오류입니다", 500)       // 사용자에게는 이것만
}

实际工作中真正重要的事

错误消息应写成从上到下阅读时形成一条路径

사용자 조회(id=42): 설정 읽기(/etc/app.yaml): open /etc/app.yaml: no such file

每层只添加一段自己知道的上下文,就会形成这样的结果。惯例是不以大写字母开头,也不以句号结尾,因为错误消息还会被嵌入另一条消息中。