if err != nil 让人烦的原因
一句话总结
Go 把错误作为值返回。因此无法轻易跳过错误处理,代价是代码变长;这种长度不是负担,而是显式性的价格。
为什么需要它
在使用异常的语言中,仅看代码无法知道哪一行可能抛出异常,必须沿调用图追踪才能判断“这里会不会失败”。
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 也可以接受多个 %w,errors.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
每层只添加一段自己知道的上下文,就会形成这样的结果。惯例是不以大写字母开头,也不以句号结尾,因为错误消息还会被嵌入另一条消息中。