Skip to content

Repository files navigation

permalink /
sidebarBasedOnContent true

og/error

go get github.com/og/error

Go错误处理,提供后端开发便捷方法,也可以作为错误处理的规范和教程

  • 创建与增强New带堆栈,Wrap加上下文,Cause找根因
  • 类型判断As转结构体,Is判特定值
  • 最佳实践if semicolon 代码防覆盖;panic只在程序启用失败使用;勿用 error 表达正常结果
  • 业务拒绝Reject公开拒绝,New隐藏错误,AsReject区分处理

目录:

  1. 透传 默认错误处理方式
  2. New 创建
  3. Wrap 包装
  4. Cause 错误初因
  5. Unwrap 解包装
  6. As error接口转换为结构体
  7. Is error接口转换为特定变量
  8. 错误处理陷阱 错误处理编程规范
  9. panic 何时 panic
  10. 滥用错误 error包万物
  11. 业务拒绝 后端实践

透传

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)

New

创建错误

oerr.New 创建的错误能附带堆栈信息便于调试 官方标准库 errors.New 创建的错误没有堆栈信息。

package main

import (
	oerr "github.com/og/error"
)

func main() {
	err := oerr.New("abc")
	oerr.PrintStack(err)
}

Wrap

底层返回的错误往往是通用的,比如"读取文件失败",但你不知道是哪个文件。Wrap的作用就是把具体信息附加上去。

查看代码示例 exmple/wrap/main.go

Wrap 的内部实现拆解:

Wrap实际上是 WithMessageWithStack 的组合:

方法 作用 说明
WithMessage 附加消息 添加描述,比如"什么场景下读取配置文件失败"
WithStack 附加堆栈信息 记录当前调用位置,便于定位问题发生在哪行代码

Cause

oerr.Wrap 对错误进行了包装,需要最初的错误就需要 oerr.Cause

查看代码示例 example/cause/main.go

Unwrap

实际工作中用的很少,应该用 Cause, 因为 包装的层数是未知的,Unwrap 只能查一层,Cause可以直接查到原始错误。

As

识别错误类型,将 error 转换为结构体

os.Open 方法在打开文件失败时候会返回错误。 会有有

  1. 路径错误 os.Patoerror
  2. 系统调用错误 os.SyscallError
  3. 各种其他错误

os.Open 函数签名是 func Open(name string) (*File, error)

这里的 error 是什么错误就需要使用 As 去判断

查看代码示例 exmple/as/main.go

Is

使用 Is 可以判断特定错误

查看代码示例 example/is/main.go

Is和As的对比

特性 Is As
查什么 具体的错误值 错误类型
典型场景 判断是不是 io.EOFos.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,说明出现了无法解决的问题,必须程序员修改代码或检查环境。

一般情况只有程序启动阶段 读取配置文件 监听端口 连接数据库 等操作出现失败才需要 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 来判断。

业务拒绝

日常开发中,有两类信息需要处理:

  1. 业务错误信息(如“手机号码已存在”)需传递给客户端;
  2. 内部执行错误(如数据库连接失败、网络链接异常)仅记录日志,对外统一返回“系统错误”。

若直接用 oerr.New("手机号码已存在") 返回,协议层将无法区分业务错误与内部错误,且存在信息不完整、不安全的风险。

为此,我们引入两个概念:

oerr.Reject():创建可公开给用户的业务错误,用于拒绝当前请求并说明原因。 oerr.New():创建不公开的内部错误,仅记录日志,对外统一响应 500。

通过这种区分,协议层可以清晰地识别并处理不同类型的错误,提升系统的安全性与可维护性。

实际开发过程中最终响应时实现拦截器去区分2种错误

查看代码示例并运行 exmple/reject/main.go

源码实现非常简单,感兴趣可以看看

查看源码 reject.go

其他方法

使用率低,可有可无的方法

查看

About

Go错误处理,提供后端开发便捷方法,也可以作为错误处理的规范和教程。

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages