Effective Engineering---(go)that‘s enough for error

本文探讨了Go语言标准库中的错误处理机制,分析了其简洁但可能不足的地方,如无法附加更多信息。接着介绍了如何通过自定义错误类型和`fmt.Errorf`来增加错误的上下文。同时,文章提到了`github.com/pkg/errors`第三方库,它能提供错误堆栈和更丰富的错误信息。最后,总结了错误处理的原则,强调了错误包装和信息添加的重要性,以及在错误处理中的不同模式。
摘要由CSDN通过智能技术生成

一、标准库分析

它是【内置类型】

The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error.

1.1、简介

它只有一个方法 Error,只要实现了这个方法,就是实现了error。现在我们自己定义一个错误试试。

1.2、使用和设计原理

type fileError struct {
}

func (fe *fileError) Error() string {
	return "aaaaa"
}
func main() {
	conent, err := openFile()
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(string(conent))
	}
}

// 模拟一个错误
func openFile() ([]byte, error) {
	return nil, &fileError{}
}

小结:通过源码,我们能学习什么呢?

  • 经常看到的一句话:go标准error的设计非常简洁,但是对于我们开发者来说,很明显是不足的

  • 想附加更多的信息,只能先通过Error方法,先取出原来的错误信息,再拼接,最后使用errors.New函数生成新错误返回

二、满足不了,真实的需求是什么呢?

  • 在什么文件的,哪一行代码
  • 每种错误都类似上面一样定义一个错误类型,但是这样太麻烦了
  • 无法更好的处理问题,不能为我们处理错误,提供更有用的信息

2.1、常用errr的错误方式:

参考这位路人的描述Go 程序错误处理的一些建议

  • 底层函数WriteAll在发生错误后,除了向上层返回错误外还向日志里记录了错误,上层调用者做了同样的事情,记录日志然后把错误再返回给程序顶层。因此在日志文件中得到一堆重复的内容。
  • 在程序的顶部,虽然得到了原始错误,但没有相关内容,换句话说没有把WriteAll、WriteConfig记录到 log 里的那些信息包装到错误里,返回给上层。
func WriteAll(w io.Writer, buf []byte) error {
    _, err := w.Write(buf)
    if err != nil {
        log.Println("unable to write:", err) // annotated error goes to log file
        return err                           // unannotated error returned to caller
    }
    return nil
}

func WriteConfig(w io.Writer, conf *Config) error {
    buf, err := json.Marshal(conf)
    if err != nil {
        log.Printf("could not marshal config: %v", err)
        return err
    }
    if err := WriteAll(w, buf); err != nil {
        log.Println("could not write config: %v", err)
        return err
    }
    return nil
}

func main() {
    err := WriteConfig(f, &conf)
    fmt.Println(err) // io.EOF
}

2.2、简单的解决方法

  • 针对这两个问题的解决方案可以是,在底层函数WriteAll、WriteConfig中为发生的错误添加上下文信息,然后将错误返回上层,由上层程序最后处理这些错误。
  • 其实 go error标准库确实是提倡这么简单解决的
  • 另外,go 1.13 以后添加了三个函数和一个格式化动词(%w),当存在该动词时,所返回的错误fmt.Errorf将具有Unwrap方法,该方法返回参数%w对应的错误。%w对应的参数必须是错误(类型)。在所有其他方面,%w与%v等同。
  • 是否包装
    在使用fmt.Errorf或通过实现自定义类型将其他上下文添加到错误时,您需要确定新错误是否应该包装原始错误。这个问题没有统一答案。它取决于创建新错误的上下文。包装错误将会被公开给调用者。如果要避免暴露实现细节,那么请不要包装错误。
func WriteConfig(w io.Writer, conf *Config) error {
    buf, err := json.Marshal(conf)
    if err != nil {
        return fmt.Errorf("could not marshal config: %v", err)
    }
    if err := WriteAll(w, buf); err != nil {
        return fmt.Errorf("could not write config: %v", err)
    }
    return nil
}
func WriteAll(w io.Writer, buf []byte) error {
    _, err := w.Write(buf)
    if err != nil {
        return fmt.Errorf("write failed: %v", err)
    }
    return nil
}

三、分析第三方库的设计

fmt.Errorf只是给错误添加了简单的注解信息,如果你想在添加信息的同时还加上错误的调用栈?

收集的这些信息不止可以输出到控制台,也可以当做日志,使用输出到相应的Log日志里,便于分析问题。

  • Error返回的其实是个字符串,我们可以修改下,让这个字符串可以设置就可以了
type fileError struct {
	s string
}

func (fe *fileError) Error() string {
	return fe.s
}
  • 改善

我们就可以通过New函数,创建不同的错误,这其实就是我们经常用到的errors.New函数,
被我们一步步剖析演化而来,现在大家对Go语言(golang)内置的错误error有了一个清晰的认知了


func New(text string) error {
	return &errorString{text}
}

type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}
  • 继续扩充,再增加一些字段来存储更多的信息
type stack []uintptr
type errorString struct {
	s string
	*stack
}

func callers() *stack {
	const depth = 32
	var pcs [depth]uintptr
	n := runtime.Callers(3, pcs[:])
	var st stack = pcs[0:n]
	return &st
}

func New(text string) error {
	return &errorString{
		s:   text,
		stack: callers(),
	}
}
  • 错误附加一些信息

使用WithMessage函数,对原来的error包装下,就可以生成一个新的带有包装信息的错误了。

type withMessage struct {
	cause error
	msg   string
}

func WithMessage(err error, message string) error {
	if err == nil {
		return nil
	}
	return &withMessage{
		cause: err,
		msg:   message,
	}
}

//只附加新的信息
func WithMessage(err error, message string) error

//只附加调用堆栈信息
func WithStack(err error) error

//同时附加堆栈和信息
func Wrap(err error, message string) error
  • 错误的根本原因
func Cause(err error) error {
	type causer interface {
		Cause() error
	}
	// for循环一直找到最根本(最底层)的那个error
	for err != nil {
		cause, ok := err.(causer)
		if !ok {
			break
		}
		err = cause.Cause()
	}
	return err
}
  • 错误类型都实现了Formatter接口,可以通过fmt.Printf函数输出对应的错误信息

%s,%v //功能一样,输出错误信息,不包含堆栈
%q //输出的错误信息带引号,不包含堆栈
%+v //输出错误信息和堆栈

四、我们从标准库和第三方库中学到了什么?

  • 对原来的xxx包装下,就可以生成一个新的带有包装信息的xxx了,使用WithMessage函数

  • 错误的三种处理

    • 1、经典Go语言的处理模式

    过程式的调用

    • 2、类似Try-Exception的代码风格

    集中处理错误

    • 3、函数式编程

    函数式编程最直观的一个特点是 延迟执行
    延迟执行 达到一个很有意思的效果 - 分离关注点
    关注点1 - 数据结构
    关注点2 - 执行逻辑

总结

错误处理的原则:

  • 错误只在逻辑的最外层处理一次,底层只返回错误。
  • 底层除了返回错误外,要对原始错误进行包装,增加错误信息、调用栈等这些有利于排查的上下文信息。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值