Error(错误处理)
Go 通常把可预期失败作为返回值处理,而不是抛出异常。error 是只有 Error() string 方法的接口;调用方应根据失败类型决定重试、转换、记录或继续向上返回。
创建、包装与判断
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("record not found")
func load(id int) (string, error) {
if id != 1 {
return "", fmt.Errorf("load id %d: %w", id, ErrNotFound)
}
return "Ada", nil
}
func main() {
_, err := load(2)
if errors.Is(err, ErrNotFound) {
fmt.Println("show a not-found response")
}
}
%w 保留原因链,调用方可以用 errors.Is 判断哨兵错误。包装应增加当前操作的上下文;若调用方不应依赖底层错误身份,不要无意使用 %w 暴露实现细节。
提取自定义错误
errors.As 适合提取稳定的错误类型和字段:
type ValidationError struct{ Field string }
func (e *ValidationError) Error() string {
return "invalid field: " + e.Field
}
func fieldFrom(err error) string {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
return validationErr.Field
}
return ""
}
片段需要与上一示例相同的 errors 导入;生产代码还应在构造错误时明确哪些字段属于公共契约。
把哪些哨兵错误、类型和字段视为稳定契约,应在包的公共 API 中明确。错误消息供人阅读,不应作为程序分支依据。
传播、记录和恢复
底层函数负责返回带上下文的错误;日志通常在 HTTP、命令行或任务消费者等边界记录一次,并附带请求 ID 等上下文。多层重复记录同一错误会制造噪声。重试前先确认失败是否临时、操作是否幂等,以及取消信号是否已到达。
Go 1.20 起 errors.Join 可把多个独立错误组合为一个可被 errors.Is/errors.As 遍历的错误。不要为了聚合而丢掉操作顺序或资源关闭失败的上下文。
常见错误
if err != nil后继续使用无效结果。- 用字符串比较错误消息,而不是
errors.Is或errors.As。 - 包装错误时使用
%v,意外丢失调用方需要的错误链。 - 每层都记录同一个错误,或底层记录后继续返回造成重复日志。
- 把
panic当作日常业务错误处理。
JavaScript 的 throw 会跳转控制流;Go 错误是普通值,必须显式返回和检查。只有程序不变量被破坏等不可恢复情况才考虑 panic,服务边界还应有审慎的恢复策略。
小结
- 使用
%w、errors.Is和errors.As保持可检查的错误链。 - 错误身份和类型是程序契约,错误消息主要面向人。
- 将记录、转换和重试放在知道业务语义的边界层。