| permalink | / |
|---|---|
| sidebarBasedOnContent | true |
go get github.com/og/errorGo错误处理,提供后端开发便捷方法,也可以作为错误处理的规范和教程。
- 创建与增强:
New带堆栈,Wrap加上下文,Cause找根因 - 类型判断:
As转结构体,Is判特定值 - 最佳实践:
if semicolon代码防覆盖;panic只在程序启用失败使用;勿用error表达正常结果 - 业务拒绝:
Reject公开拒绝,New隐藏错误,AsReject区分处理
目录:
- 透传 默认错误处理方式
- New 创建
- Wrap 包装
- Cause 错误初因
- Unwrap 解包装
- As error接口转换为结构体
- Is error接口转换为特定变量
- 错误处理陷阱 错误处理编程规范
- panic 何时 panic
- 滥用错误 error包万物
- 业务拒绝 后端实践
Go 的错误处理通常是直接透传的:调用某个方法时出现错误,就将该错误通过当前函数直接返回。例如:
func GetUser(id uint64) (user User, err error){
if user, err = Query(id); err != nil {
// 透传
return
}
}如果 GetUser原本没有返回错误:
func GetUser(id uint64) (user User)func GetUser(id uint64) (user User, err error)创建错误
oerr.New 创建的错误能附带堆栈信息便于调试 官方标准库 errors.New 创建的错误没有堆栈信息。
package main
import (
oerr "github.com/og/error"
)
func main() {
err := oerr.New("abc")
oerr.PrintStack(err)
}底层返回的错误往往是通用的,比如"读取文件失败",但你不知道是哪个文件。Wrap的作用就是把具体信息附加上去。
查看代码示例 exmple/wrap/main.go
Wrap 的内部实现拆解:
Wrap实际上是 WithMessage 和 WithStack 的组合:
| 方法 | 作用 | 说明 |
|---|---|---|
| WithMessage | 附加消息 | 添加描述,比如"什么场景下读取配置文件失败" |
| WithStack | 附加堆栈信息 | 记录当前调用位置,便于定位问题发生在哪行代码 |
oerr.Wrap 对错误进行了包装,需要最初的错误就需要 oerr.Cause
查看代码示例 example/cause/main.go
实际工作中用的很少,应该用 Cause, 因为 包装的层数是未知的,Unwrap 只能查一层,Cause可以直接查到原始错误。
识别错误类型,将 error 转换为结构体
os.Open 方法在打开文件失败时候会返回错误。
会有有
- 路径错误
os.Patoerror - 系统调用错误
os.SyscallError - 各种其他错误
os.Open 函数签名是 func Open(name string) (*File, error)
这里的 error 是什么错误就需要使用 As 去判断
查看代码示例 exmple/as/main.go
使用 Is 可以判断特定错误
查看代码示例 example/is/main.go
Is和As的对比
| 特性 | Is | As |
|---|---|---|
| 查什么 | 查具体的错误值 | 查错误类型 |
| 典型场景 | 判断是不是 io.EOF、os.ErrNotExist |
拿到 *os.Patoerror 读取 Path 字段 |
最常见的错误处理代码,会因为后续迭代过程中粗心导致难以发现的bug
最初版本
var a struct{}
err = json.Unmarshal([]byte(`incorrect json`), &a)
if err != nil {
return
}
return
迭代时需要加上
b, err := strconv.ParseFloat("1", 64)
if err != nil {
return
}因为粗心加在了错误的地方
var a struct{}
err = json.Unmarshal([]byte(`incorrect json`), &a)
b, err := strconv.ParseFloat("1", 64) // ❌不应该再这一行
if err != nil {
return
}
// ✅应该在这一行
return如果遇到这个问题,简单的复盘认为是第二次修改代码人的问题固然可以。可问题已经出现了。
使用 if semicolon 解决
var a struct{}
if err = json.Unmarshal([]byte(`incorrect json`), &a); err != nil {
return
}
return查看代码示例 example/if_semicolon/main.go
总结:if_semicolon 模式通过将函数调用与错误检查绑定为一个原子单元,从根本上杜绝了"误插代码覆盖错误"这类人为失误,适合作为团队编码规范推广。
核心原则:只要执行
panic,说明出现了无法解决的问题,必须程序员修改代码或检查环境。
一般情况只有程序启动阶段 读取配置文件 监听端口 连接数据库 等操作出现失败才需要 panic
勿用 error 表达正常结果
有些库用error包万物,使用起来心智负担中。长时间容易出错。
例如:SQL查询数据会有数据不存在的情况,这种情况属于函数完成,结果是空。 error包万物 会导致代码复杂逻辑混乱
// ❌ 糟糕的设计
row := db.QueryRow("select name from user where id= ?", 1)
if err = row.Scan(&name); err != nil {
// 每个查询都需要通过 err = sql.ErrNoRows 判断是数据不存在还是SQL执行失败
if err = sql.ErrNoRows {
log.Print("没数据")
return
} else {
log.Print(err)
return
}
}// ✅合理的设计,数据不存在不属于错误,属于执行结果的一种
row := db.QueryRow("select name from user where id= ?", 1)
+ var has bool
+ if has, err = row.Scan(&name); err != nil {
log.Print(err)
return
}
+ if has == false {
+ log.Print("没数据")
+ return
}应当 避免 sql.ErrNoRows 这种偷懒的设计
数据不存在应该明确的通过 bool 表达,而不是让使用者通过 err == sq.ErrNoRows 来判断。
日常开发中,有两类信息需要处理:
- 业务错误信息(如“手机号码已存在”)需传递给客户端;
- 内部执行错误(如数据库连接失败、网络链接异常)仅记录日志,对外统一返回“系统错误”。
若直接用 oerr.New("手机号码已存在") 返回,协议层将无法区分业务错误与内部错误,且存在信息不完整、不安全的风险。
为此,我们引入两个概念:
oerr.Reject():创建可公开给用户的业务错误,用于拒绝当前请求并说明原因。
oerr.New():创建不公开的内部错误,仅记录日志,对外统一响应 500。
通过这种区分,协议层可以清晰地识别并处理不同类型的错误,提升系统的安全性与可维护性。
实际开发过程中最终响应时实现拦截器去区分2种错误
查看代码示例并运行 exmple/reject/main.go
源码实现非常简单,感兴趣可以看看
查看源码 reject.go
使用率低,可有可无的方法