Go函数返回error必须显式暴露业务失败,禁用panic替代;包装错误用%w而非%s;95%场景无需自定义error类型。

Go 函数返回 error 必须显式暴露业务失败,不能用 panic 替代;包装错误要用 %w,不是 %s;95% 的场景无需自定义 error 类型。
error 必须作为返回值显式传递,不能 panic
业务逻辑出错(比如参数非法、数据库查无记录、HTTP 请求返回 404)属于可恢复失败,调用方需要决定重试、降级或提示用户。用 panic 会中断 goroutine,无法被上层捕获或处理。
-
panic(fmt.Errorf("invalid id: %s", id))是错的 —— 调用方拿不到 error,日志也缺上下文 - 正确写法:
return nil, fmt.Errorf("invalid user id: %s", id) - HTTP handler、CLI 命令、gRPC 方法里尤其要守这个线:所有业务失败走
error返回,只在真正崩溃时(如空指针解引用)才panic
用 %w 包装错误,别用 %s 或忽略
底层库(如 os.Open、json.Unmarshal)返回的 error 缺少当前层语义。直接返回会让调用方无法区分“文件不存在”和“权限不足”,也看不到在哪一层出的问题。
- 错:
return nil, fmt.Errorf("failed to load config: %s", err)→ 原始 error 被吞掉,errors.Is(err, fs.ErrNotExist)失效 - 对:
return nil, fmt.Errorf("failed to read config file %q: %w", path, err)→ 保留调用链,支持errors.Unwrap和errors.As - 每层只包一次,避免嵌套过深;不要在 defer 里重复包装(比如
defer func() { if err != nil { err = fmt.Errorf("cleanup failed: %w", err) } }())
什么时候该定义自定义 error 类型?
只有两种情况值得定义结构体 error:需要提供额外方法(如 .StatusCode()、.Retryable()),或必须用 errors.As 精确匹配类型做分支处理。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 常见反模式:为每个业务模块都定义
UserNotFoundError、OrderInvalidError—— 如果不提供方法也不需类型断言,纯属冗余 - 合理用法示例:
type HTTPError struct{ Code int; Body []byte },然后实现StatusCode() int和Unwrap() error - 如果只是想加字段用于日志,用
fmt.Errorf("user not found (id=%d): %w", id, err)更轻量、更通用
错误日志只在最外层打一次,别层层打印
Model 层调用 db.QueryRow 失败,Service 层调用 Model 失败,API 层再调用 Service —— 如果每层都 log.Error("xxx failed: %v", err),同一错误会在日志里出现三次,还分散在不同位置,排查时根本分不清哪条是源头。
- 原则:只在错误「离开当前责任域」时记录,通常是 handler、main 入口、或 gRPC interceptor
- 中间层(repository、service)只负责返回 error,不 log,不 recover,不 retry(除非明确设计为重试策略)
- 如果真要带上下文打日志,用
fmt.Printf("%+v", err)—— 它能展开%w链,显示完整调用栈
最容易被忽略的是错误包装的“度”:包得太少,丢失上下文;包得太多,堆栈膨胀难读;包错位置(比如在 defer 里二次包装),反而掩盖原始错误。真正的优雅,是让 error 既可读、又可判、还可追溯,而不是让它变重。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










