Go中错误是接口类型而非异常,通过返回值显式传递;需用errors.Is/errors.As语义化判断、%w包装传递上下文、自定义错误类型携带字段与行为,并注意日志脱敏和监控分组。

Go里错误不是异常,error 是一个接口类型
Go不提供 try/catch,所有错误都通过返回值显式传递。核心是标准库定义的 error 接口:type error interface { Error() string }。这意味着任何实现了 Error() 方法的类型都能当错误用——比如 fmt.Errorf 返回的、os.Open 返回的,甚至你自己写的结构体。
常见误区是把错误当成“可忽略的返回值”:只检查 err != nil 就完事,却不关心具体类型或上下文。这会导致日志模糊、重试逻辑失效、调试时找不到根因。
- 不要用
panic处理预期中的错误(如文件不存在、网络超时),它适合真正不可恢复的程序状态(如空指针解引用) - 避免用字符串比较判断错误类型:
if err.Error() == "no such file"—— 一旦底层错误消息变更就崩 - 优先用类型断言或
errors.Is/errors.As(Go 1.13+)做语义化判断
如何包装和传递错误上下文
原始错误(如 os.ErrNotExist)往往缺少调用链信息。直接返回会丢失“谁在什么位置触发了这个错误”。Go 1.13 引入的 fmt.Errorf 带 %w 动词支持错误包装(wrapping),让 errors.Unwrap 和 errors.Is 能穿透多层。
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file %q: %w", path, err)
}
return data, nil
}
这样上层就能准确识别是否是文件不存在:
立即学习“go语言免费学习笔记(深入)”;
-
errors.Is(err, os.ErrNotExist)→true(即使被包装了三层) -
errors.As(err, &os.PathError{})→ 可提取原始PathError结构体,拿到Err、Path、Op字段 - 用
errors.Unwrap(err)手动展开一层,适合自定义错误处理逻辑
什么时候该用自定义错误类型
当错误需要携带额外字段(如重试次数、HTTP 状态码、SQL 错误码)、或需实现特定行为(如临时性错误标记、自动重试策略)时,就该自己写错误类型。别只靠字符串拼接。
例如一个数据库操作错误:
type DBError struct {
Code int
Message string
Retryable bool
}
func (e *DBError) Error() string { return e.Message }
func (e *DBError) Temporary() bool { return e.Retryable }
这样调用方可以用类型断言判断是否可重试:
if dbErr, ok := err.(*DBError); ok && dbErr.Retryable { ... }- 配合
net.Error接口规范,能让标准库函数(如http.Client)自动识别临时错误并重试 - 注意:自定义错误类型要导出字段或提供访问方法,否则外部包无法安全使用
错误日志与监控的关键细节
打印错误时只用 fmt.Printf("%v", err) 会丢掉包装链;用 %+v(来自 github.com/pkg/errors 或 Go 1.20+ 的 fmt)才能展开完整堆栈和上下文。但生产环境别盲目打全量堆栈——可能泄露敏感路径或参数。
- 对用户暴露的错误消息必须脱敏,内部日志才保留完整
%+v - 用
log/slog(Go 1.21+)时,把错误作为属性传入:slog.String("error", err.Error()),而非拼进消息字符串 - 监控告警时,按错误类型(而非字符串)分组:用
errors.Is判断是否属于os.ErrPermission,而不是匹配"permission denied"
最常被跳过的一步:没给错误设置超时或重试上限,导致一个 context.DeadlineExceeded 错误反复包装,最终日志里出现几十层 "failed to call X: failed to call Y: ...",却看不出最初触发点在哪里。










