微服务学习-day 2-Go的错误处理

Golang 中的错误处理

1、Golang 中的 错误定义

  • Go 的处理异常逻辑是不引入 exception,支持多参数返回,所以你很容易的在函数签名中带上实现了 error interface 的对象,交由调用者来判定。 如果一个函数返回了 value, error,你不能对这个 value 做任何假设,必须先判定 error。唯一可以忽略 error 的是,如果你连 value 也不关心。
  • Go 中有 panic 的机制,如果你认为和其他语言的 exception 一样,那你就错了。当我们抛出异常的时候,相当于你把 exception 扔给了调用者来处理。 比如,你在 C++ 中,把 string 转为 int,如果转换失败,会抛出异常。或者在 java 中转换 string 为 date 失败时,会抛出异常。
  • Go panic 意味着 fatal error(就是挂了)。不能假设调用者来解决 panic,意味着代码不能继续运行。 使用多个返回值和一个简单的约定,Go 解决了让程序员知道什么时候出了问题,并为真正的异常情况保留了 panic。
  • 错误在Golang中的定义非常的简单,就是一个error 接口

  • 任何实现的Error() 的类型,都是Error


// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
	Error() string
}

2、最优解(fix:解决了太长不想看的BUG)

  1. Handle Once 原则
    1. 对于 Error 只进行一次处理(log,降级),处理完成的Error 不能继续抛出
    2. 如果需要log 对Error 打日志 ,可以去添加附加信息到Error 中 再抛出,统一到 Log 中间件去处理错误
  2. 如果项目被基础项目 如 encode , sql驱动之类的基础库
    1. 直接返回基础的错误
    2. 使用 fmt.Errorf(“%w [Ext Message]”,err) 对error进行包装,返回附加的额外信息
    3. 返回 Sentinel Error (人话 : var SErr = errors.New(“some err”))
    4. 返回 Error types (人话:实现了Error 接口的结构体,优势:可以附带更多的信息)
  3. 如果是业务项目(使用 github.com/pkg/errors)
    1. 对基础的,未包装的 Error 使用 errors.Wrap() 进行包装
    2. 如果是上层业务代码 携带过来的 包装过的Error 则可以 使用 errors.WithMessage() 进行附加信息的添加

3、Error 的 种类

  • Sentinel Error

    • 预定义的特定错误,我们叫为 sentinel error,这个名字来源于计算机编程中使用一个特定值来表示不可能进行进一步处理的做法。所以对于 Go,我们使用特定的值来表示错误

    • 使用 sentinel 值是最不灵活的错误处理策略,因为调用方必须使用 == 将结果与预先声明的值进行比较。当您想要提供更多的上下文时,这就出现了一个问题,因为返回一个不同的错误将破坏相等性检查。

    • 使用 sentinel error 会添加额外的包的依赖

  • Error types

1. Error type 是实现了 error 接口的自定义类型。例如 MyError 类型记录了文件和行号以展示发生了什么。

    type MyError struct {
    	Msg  string
    	File string
    	Line int
    }
    
    func (m *MyError) Error() string {
    	return fmt.Sprintf("%s:%d: %s", m.File, m.Line, m.Msg)
    }

2. 因为 MyError 是一个 type,调用者可以使用断言转换成这个类型,来获取更多的上下文信息。

    
    type MyError struct {
    	Msg  string
    	File string
    	Line int
    }
    
    func (m *MyError) Error() string {
    	return fmt.Sprintf("%s:%d: %s", m.File, m.Line, m.Msg)
    }
    
    func test() error {
    	return &MyError{
    		Msg:  "1",
    		File: "lib.go",
    		Line: 41,
    	}
    }
    
    func main()  {
    	err := test()
    	if MyError, ok := err.(*MyError) ;ok {
    		log.Println(MyError.Error())
    	}
    }	

3. 与 Sentinel Error 相比 Error Types 让 Error 可以携带更多的信息,以及一些上下文数据 例如 (os.PathError)

4. 但是同样 Error Types 依赖 类型断言,对包产生了强依赖,从而导致API变脆弱,当我们需要修改错误类型时,会出现非常大的阻碍

  • Opaque errors

    • 这是最灵活的错误处理策略,因为它要求代码和调用者之间的耦合最少。 我将这种风格称为不透明错误处理,因为虽然您知道发生了错误,但您没有能力看到错误的内部。作为调用者,关于操作的结果,您所知道的就是它起作用了,或者没有起作用(成功还是失败)。 这就是不透明错误处理的全部功能–只需返回错误而不假设其内容。

    • 【断言错误的行为,而不是错误的类型】在少数情况下,这种二分错误处理方法是不够的。例如,与进程外的世界进行交互(如网络活动),需要调用方调查错误的性质,以确定重试该操作是否合理。在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。考虑这个例子

    
    type MyError struct {
    	Msg  string
    	File string
    	Line int
    }
    
    func (m *MyError) Error() string {
    	return fmt.Sprintf("%s:%d: %s", m.File, m.Line, m.Msg)
    }
    
    func (m *MyError) isHandle() bool {
    	return true
    }
    
    func IsMyError(err error) bool {
    	myErr, ok := err.(*MyError)
    
    	return ok && myErr.isHandle()
    }
    
    func test() error {
    	return &MyError{
    		Msg:  "1",
    		File: "lib.go",
    		Line: 41,
    	}
    }
    
    func main() {
    	err := test()
    	if IsMyError(err) {
    		//	do something
    	}
    }
    

4、错误处理

用函数去封装错误

  // 统计行数
   // 通常做法
   func countLines(reader io.Reader) (n int, err error) {
   	var (
   		buf = bufio.NewReader(reader)
   	)
   	for {
   		_, err = buf.ReadString('\n')
   		if err != nil {
   			break
   		}
   		n++
   	}
   	//这一步并不多余,如果出错的情况下,需要将结果值 赋予成 零值
   	if err != io.EOF {
   		return 0, err
   	}
   	return
   }
   
   // 更好的做法
   func countLines2(reader io.Reader) (n int, err error) {
   	sc := bufio.NewScanner(reader)
   	for sc.Scan() {
   		n++
   	}
   	err = sc.Err()
   	return
   }
   

相同行为的错误合并在一起处理

  
  type status struct {
  	code int
  	msg  string
  }
  
  type header struct {
  	key, value string
  }
  
  func writeResp(w io.Writer, stat status, headers []header, body io.Reader) (err error) {
  	_, err = fmt.Fprintf(w, "HTTP/1.1 %d %s \r\n", stat.code, stat.msg)
  	if err != nil {
  		return
  	}
  	for _, h := range headers {
  		_, err = fmt.Fprintf(w, "%s: %s \r\n", h.key, h.value)
  		if err != nil {
  			return
  		}
  	}
  
  	_, err = fmt.Fprintf(w, "\r\n")
  
  	if err != nil {
  		return
  	}
  
  	_, err = io.Copy(w, body)
  
  	return
  }
  
  type errWriter struct {
  	err error
  	w   io.Writer
  }
  
  func (e *errWriter) Write(p []byte) (n int, err error) {
  	if e.err != nil {
  		return 0, e.err
  	}
  	n, err = e.w.Write(p)
  	if err != nil {
  		e.err = err
  	}
  	return
  }
  // 将错误包装在 writer 结构体中,如果出错,则 write 直接会返回 最后返回error
  func writeResp2(w io.Writer, stat status, headers []header, body io.Reader) (err error) {
  
  	errorWriter := &errWriter{
  		w: w,
  	}
  
  	fmt.Fprintf(errorWriter, "HTTP/1.1 %d %s \r\n", stat.code, stat.msg)
  	for _, h := range headers {
  		fmt.Fprintf(w, "%s: %s \r\n", h.key, h.value)
  	}
  
  	fmt.Fprintf(w, "\r\n")
  
  	io.Copy(w, body)
  
  	return errorWriter.err
  }
  

包装错误

// 使用 github.com/pkg/errors 库
// 使用 这个 error 库可以打印 堆栈信息 方便调试
func main() {
  err := test()
  err = errors.Wrap(err,"当前库的一些上下文")
  log.Printf("%+v",err)
}
  • 错误只处理一次原则(HANDLE ONCE)

    • 当一个错误出现后,可以选择对错误进行处理 例如(log,服务降级),但是处理了错误以后,不能再将同样的错误往上抛出

    • 如果选择不处理错误,则可以将错误直接return ,或者 使用 上文说的第三方库 的 errors.WithMessage() 函数 添加附加信息


好好学习,天天向上