Go 的 error,我花了很久才用顺

举报
yd_241780069 发表于 2026/11/06 20:45:08 2026/11/06
【摘要】 起因从 Java 转过来,最不适应的是 Go 的 error 处理。Java 里 try-catch 一把兜住,Go 里每个可能出错的地方都得写 if err != nil,一开始觉得啰嗦得不行。写久了才慢慢理解它为什么这么设计,也踩了几个坑。 最基本的Go 没有异常,错误是值。函数返回 (结果, error),调用方自己判断:f, err := os.Open("file.txt")if...

起因

从 Java 转过来,最不适应的是 Go 的 error 处理。Java 里 try-catch 一把兜住,Go 里每个可能出错的地方都得写 if err != nil,一开始觉得啰嗦得不行。写久了才慢慢理解它为什么这么设计,也踩了几个坑。

最基本的

Go 没有异常,错误是值。函数返回 (结果, error),调用方自己判断:

f, err := os.Open("file.txt")
if err != nil {
    return err
}
defer f.Close()

error 是个接口:

type error interface {
    Error() string
}

任何实现了 Error() string 的类型都是 error。就这么简单。

包一层,别丢上下文

最开始我都是直接 return err,结果报错信息只有最底层的,比如 no such file or directory,根本不知道是哪个文件、在哪个环节出的错。

后来用 fmt.Errorf 加上下文:

if err != nil {
    return fmt.Errorf("读取配置 %s 失败: %w", path, err)
}

%w 是关键,它把原始 error 包进去了,而不是只拼字符串。包进去之后才能用 errors.Is 和 errors.As 往回查。

如果写成 %v,原始 error 就丢了,只能看到字符串,errors.Is 永远返回 false。这个区别我一开始没注意,包了半天发现判断不出来了。

errors.Is 和 errors.As

判断错误类型有两种方式,用途不一样。

errors.Is 比的是值,适合和哨兵错误比较:

if errors.Is(err, os.ErrNotExist) {
    // 文件不存在
}

errors.As 比的是类型,适合从错误链里捞出具体的错误类型:

var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println(pathErr.Path)
}

两个都能穿透 %w 包的层,沿着链条一路找。所以包的时候用 %w 很重要,不然链断了,这俩都不好使。

自定义错误类型

需要带额外字段的时候,定义自己的错误类型:

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

返回的时候用指针,判断的时候用 errors.As:

var ve *ValidationError
if errors.As(err, &ve) {
    // 拿到 ve.Field 做处理
}

注意 errors.As 的第二个参数必须是指针的指针,&ve 里 ve 本身就是 *ValidationError,所以传的是 **ValidationError。写错了编译不报,运行时 panic,这个坑挺隐蔽的。

哨兵错误

包级别的变量,用来表示一种特定的错误状态:

var ErrNotFound = errors.New("not found")

调用方用 errors.Is(err, ErrNotFound) 判断。标准库里 sql.ErrNoRows、io.EOF 都是这种。

哨兵错误的缺点是它是个全局变量,包和包之间有依赖。而且如果调用方想加额外信息,只能包一层,不能改原值。所以现在 Go 社区更推荐自定义错误类型,哨兵错误用在简单的、语义固定的场景。

panic 和 error 的边界

Go 里有 panic,但它不是给正常错误处理用的。panic 是程序遇到无法继续的情况,比如数组越界、空指针,或者初始化的时候配置缺了、没法启动。

业务错误、IO 错误、网络错误,全都是 error,不用 panic。我刚开始写的时候动不动想用 panic 图省事,后来发现调用方根本没法优雅处理,一 panic 整个 goroutine 就没了,还得靠 recover 兜底,比老老实实返回 error 麻烦多了。

recover 我只在两种地方用:一是 goroutine 的入口,防止一个 goroutine panic 把整个进程带崩;二是写框架或者中间件的时候,统一兜底转成 500。业务代码里基本不碰。

一个实际撞过的坑

defer 里关闭文件,但 Close() 本身也可能出错:

f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close()

f.Close() 返回的 error 被忽略了。写文件的时候这个问题比较严重,数据可能没刷到磁盘。读文件的话影响不大。现在的写法是,写操作显式处理 Close 的 error:

defer func() {
    if cerr := f.Close(); cerr != nil && err == nil {
        err = cerr
    }
}()

读操作我还是直接 defer f.Close(),不折腾。

小结

Go 的 error 处理啰嗦,但啰嗦得有道理,每个错误在哪一层处理、加什么上下文,写代码的人得想清楚。几个关键点:包错误用 %w、判断用 errors.Is 和 errors.As、自定义类型用指针、panic 只留给真正没法继续的情况。

先记这些。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。