谈谈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标准库里的错误处理的拙劣模仿。

使用中感受到的问题

  1. sentinel error的问题。io.EOF可能需要被带上下文比如 fmt.Errorf("read 1.txt eof: %w", io.EOF); 哨兵错误可能可以被赋值,io.EOF=fmt.Errorf("我来捣乱")
  2. 很难分辨错误的类型。比如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

errx github地址

借鉴学习的文档

译|Don’t just check errors, handle them gracefully
译|Errors are values

posted @   xiaotushaoxia  阅读(178)  评论(0编辑  收藏  举报
编辑推荐:
· 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训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示