谈谈go中的错误处理,如何有效优雅的处理错误
先随便说几句
go的错误处理“饱受诟病”,我在用了半年go的时候和朋友聊到说“我开始使用go,而让我最舒服的就是go的错误处理, 让我可以几乎(这一年的使用中其实可以把几乎去掉)无脑的 err!=nil”
有人说我们需要一个这样的按键
我完全不同意,因为我觉得我们可能需要三个按键(至少两个吧?)
if err != nil { // 可能三个返回值比较少
return nil, nil, err
}
if err != nil {
return nil, err
}
if err != nil {
return err
}
大致谈谈我现在怎么做错误处理
绝大部分情况是:有错误就返回, 最上层打日志。
少数情况特殊处理,比如gorm.ErrNoRecord, io.EOF, http.ErrServerClosed类似的哨兵错误(sentinel error), 还有os.IsTimeout(err)或者ne, ok := err.(net.Error); ok && ne.Temporary()这种样子的处理。暂时好像只想到这两种。
简单来说,拿到特定场景的特定错误, 结合实际判断要不要抛给上层。
比如那io.Copy来说,它里面读到io.EOF是当错误返回的, 因为EOF对Copy来说是合理的哨兵错误。对tcpserver来说,Temporary的error也不应该退出整个服务, 好一点的办法可能是稍后重试。 对了Temporary现在已经被标记成Deprecated了,但是我没有找到好的替代品。
我错误处理基本是对go标准库里的错误处理的拙劣模仿。
使用中感受到的问题
- sentinel error的问题。io.EOF可能需要被带上下文比如 fmt.Errorf("read 1.txt eof: %w", io.EOF); 哨兵错误可能可以被赋值,io.EOF=fmt.Errorf("我来捣乱")
- 很难分辨错误的类型。比如net.Error是个interface,OpError是一种net.Error,它有Op来指示错误的类型如dial,read,write,close, 感觉只能用字符串比较去判断错误类型,就感觉怪怪的,拿到这个错好像也只能往上抛打印出来被人看到。看看这些
知乎-Go中如何准确判断和识别各种网络错误
StackOverflow-Portable way to detect different kinds of network error in Golang
比如对一个closed的net.Conn操作出错,我见过不止一个人用strings.Contains(err.Error(), "use of closed network connection")来判断。。非常丑陋, 但好像就是只能这样。
错误处理具体做法 - 总结别人的文章加上自己的理解
1. 不要忽略错误,保证每个错误都被仔细考虑了再决定忽略还是抛给调用者
这一点很好理解, 丑但是可靠
err = db.Find(&dest1).Error
if err != nil {
return err
}
err = db.Find(&dest1).Error
if err != nil {
return err
}
err = db.Take(&optionalItem).Error
if err != nil && err != gorm.ErrNoRecord {
return err
}
不过有些地方的error我处理不了, 可能也确实是多余的, 因为没见人能处理的, go标准库里面也是不处理的
func handle(conn net.Conn) error {
defer func() {
errClose := conn.Close()
if errClose != nil { // 最多记录一下对吧
fmt.Println("close error: ", errClose)
}
}()
// defer conn.Close()
// bala bala
return nil
}
2. 必要的话,加上堆栈信息和上下文
目的是方便找问题。项目比较复杂的话,调用关系很深,如果只看到一个错误字符串,问题会很难找。可能需要用到github.com/pkg/errors, 它提供WithStack, WithMessage, Is等方法, 比如前面提到的fmt.Errorf("read 1.txt eof: %w", io.EOF)问题可以用errors来解决
func TestErrors(t *testing.T) {
err := read1("1.txt")
fmt.Println(errors.Is(err, io.EOF)) // true
fmt.Printf("%+v", err)
// 输出
// EOF
// read 1.txt error
// github.com/xiaotusaoxia/errx.read1
// src/errx/errors_test.go:19
// github.com/xiaotusaoxia/errx.TestErrors
// src/errx/errors_test.go:13
// testing.tRunner
// C:/Program Files/Go120/src/testing/testing.go:1576
// runtime.goexit
// C:/Program Files/Go120/src/runtime/asm_amd64.s:1598--- PASS: TestErrors (0.00s)
}
func read1(path string) error {
// fmt.Println("read ", path, "error, got error io.EOF")
return errors.Wrap(io.EOF, "read "+path+" error") // Wrap会加上message和stack信息
}
3. 不要重复处理错误
还是2种的例子read1既然决定把err给调用方,它就不需要再把错误记录下来了。
我自己对errors的封装-errx
为什么要封装errors呢, 其实它已经很好用了。只有一个原因: stack信息重复, 导致我要去考虑什么时候要加stack什么时候不用加, 所以我弄了一下, 把errors.WithStack隐藏起来了,并在Warp的时候自动检查有没有添加过stack。
使用方法就是: 如果你要加上当前信息,就像myOpen2那样; 比如myOpen3直接把myOpen2的err返回了,就说明不需要加上myOpen3的上下文message。
所以errors.Withxxx其他的我基本都不用了, 基本errors.Warp又因为会重复添加stack, 我也不用了。 现在我基本就只使用fmt.Errorf和errx.Warp, errors.Is
但这个errx.Wrap会一层层找是否已经被添加过stack, 所以可能性能有点问题。 有空做一下b test。
package errx
import (
"fmt"
"net/http"
"testing"
)
func TestWrap(t *testing.T) {
_, err := myOpen2("xx")
fmt.Println(Format(err))
}
func myOpen(path string) ([]byte, error) {
_, err := http.Get(path)
if err != nil {
return nil, Wrap(err, "myOpen")
}
return []byte("Ok"), nil
}
func myOpen2(path string) ([]byte, error) {
open, err := myOpen(path)
if err != nil {
return open, Wrap(err, "MyOpen2 failed")
}
return nil, nil
}
func myOpen3(path string) ([]byte, error) {
open, err := myOpen2(path)
if err != nil {
return open, err
}
return nil, nil
}
//=== RUN TestWrap
//Error: MyOpen2 failed: myOpen: Get "xx": unsupported protocol scheme ""
//Stack:
//github.com/xiaotusaoxia/errx.myOpen
// /src/errx/errors_test.go:17
//github.com/xiaotusaoxia/errx.myOpen2
// /src/errx/errors_test.go:23
//github.com/xiaotusaoxia/errx.myOpen3
// /src/errx/errors_test.go:31
//github.com/xiaotusaoxia/errx.TestWrap
// /src/errx/errors_test.go:10
//testing.tRunner
// C:/Program Files/Go120/src/testing/testing.go:1576
//runtime.goexit
// C:/Program Files/Go120/src/runtime/asm_amd64.s:1598
//--- PASS: TestWrap (0.00s)
//PASS
借鉴学习的文档
译|Don’t just check errors, handle them gracefully
译|Errors are values
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人