文章目录
一、标准库分析
它是【内置类型】
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 - 执行逻辑
总结
错误处理的原则:
- 错误只在逻辑的最外层处理一次,底层只返回错误。
- 底层除了返回错误外,要对原始错误进行包装,增加错误信息、调用栈等这些有利于排查的上下文信息。