在Go中,对于处理错误一般分为两种情况: 错误和异常.
在Go中,错误的处理一般都是通过 error接口来指定;异常通常都是通过panic来指定。
go的Error
go Error就是一个普通的接口,普通的值。(https://pkg.go.dev/builtin#error)
type error interface {
Error() string
}
我们经常使用 errors.New()来返回一个error对象
分析error包核心代码
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
如下面的例子:
package main
func main(){
NotFoundErr := errors.New("没有找到文件目录")
}
在go的基础库中,也引用了大量自定义的Error,例如:
https://go.dev/src/bufio/bufio.go
var (
ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
ErrBufferFull = errors.New("bufio: buffer full")
ErrNegativeCount = errors.New("bufio: negative count")
)
有一个问题,值得思考;为什么errors.New()返回的是内部errorString对象的指针呢?
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
因为go比较两个结构体是否相等时,是依次比较每个字段,如果不返回结构体指针的话,当字段s相同时就会被认为时同一个错误
Error Vs Exception
Go的处理异常逻辑是不引入Exception,支持多参数返回。
所以我们可以很轻易的在函数签名中携带实现了error interface的对象,交给调用者来判定。
如果一个函数返回了 value, error,你不能对这个 value 做任何假设,必须先判定 error。唯一可以忽略 error 的是,如果你连 value 也不关心。
其中,Go也有panic机制,Go panic 意味着 fatal error(就是挂了)。不能假设调用者来解决 panic,意味着代码不能继续运行。
在Go中,使用多个返回值和一个简单的约定,
Go 解决了让程序员知道什么时候出了问题,并为真正的异常情况保留了 panic。
对于Panic和error什么时候用比较合适?
- 在服务初始化失败时,一些main函数里面的强依赖的基础组建,例如:mysql/redis/kafka/mq等,当连接错误时,必须panic。
- 在代码中读取配置文件出错时,例如:读取配置中心出错,读取Apollo出错,这些基础配置的读取,如果出错,一定要panic,因为是强依赖,会影响后续的程序正常运行。
- 在go语言中,也有异常捕获(recover)机制, 在对于弱依赖出错时,可以recover.
4 例如不可恢复的错误,例如:索引越界,不可恢复的环境问题,内存溢出,都推荐使用panic来让程序直接退出。
go的sentinel Error
这个单词也可以叫:哨兵error.
先说下结论: 不建议使用sentinel Error
sentinel是什么?
对于预定义的特定错误,我们叫它为 sentinel error (哨兵Error),这个名字来源于计算机编程中使用一个特定值来表示不可能进行进一步处理的做法。
对于Sentienl Error的例子可以参考 io.EOF,如下图:
使用 sentinel 值是最不灵活的错误处理策略,因为调用方必须使用 == 将结果与预先声明的值进行比较。当您想要提供更多的上下文时,这就出现了一个问题,因为返回一个不同的错误将破坏相等性检查。
为什么不推荐使用 sentinel?
- Sentinel errors 会成为你 API 公共部分。
- Sentinel errors 在两个包之间创建了依赖。
Error Types
Error type 是实现了 error 接口的自定义类型。例如 MyError 类型记录了文件和行号以展示发生了什么。
package main
import "fmt"
type MyError struct {
Msg string
File string
Line int
}
func (e *MyError) Error() string {
return fmt.Sprintf("%s:%d:%s",e.File,e.Line,e.Msg)
}
func test() error {
return &MyError{"something happend","server.go",42}
}
func main() {
err := test()
if err != nil {
fmt.Println(err)
}
}```
```go
因为 MyError 是一个 type,调用者可以使用断言转换成这个类型,来获取更多的上下文信息。
package main
import "fmt"
type MyError struct {
Msg string
File string
Line int
}
func (e *MyError) Error() string {
return fmt.Sprintf("%s:%d:%s",e.File,e.Line,e.Msg)
}
func test() error {
return &MyError{"something happend","server.go",42}
}
func main() {
err := test()
switch err := err.(type) {
case nil:
fmt.Println("没有error")
case *MyError:
fmt.Println("error occureed on line:",err.Line)
}
}
与错误值相比,错误类型的一大改进是它们能够包装底层错误以提供更多上下文。
一个不错的例子就是 os.PathError 他提供了底层执行了什么操作、那个路径出了什么问题。
结论: 调用者要使用类型断言和类型 switch,就要让自定义的 error 变为 public。这种模型会导致和调用者产生强耦合,从而导致 API 变得脆弱。
结论是尽量避免使用 error types,虽然错误类型比 sentinel errors 更好,因为它们可以捕获关于出错的更多上下文,但是 error types 共享 error values 许多相同的问题。
因此,我的建议是避免错误类型,或者至少避免将它们作为公共 API 的一部分。
Handling Error
- Indented Flow is for errors
无错误的正常流程代码,将成为一条直线,而不是缩进的代码。
例如:
f, err := os.Open(path)
if err != nil {
// handle error
}
// do stuff
f,err := os.Open(path)
if err == nil {
// do stuff
}
// handle error
2 Eliminate error handling by eliminating errors
通过消除错误来消除错误处理
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return err
}
return nil
}
// 修改后
func AuthenticateRequest(r *Request) error {
return authenticate(r.User)
}
Wrap errors
通过使用 pkg/errors包,您可以向错误值添加上下文,这种方式即可以由人,也可以由机器检查
func Write(w io.Write, buf []byte)error {
_,err := w.Write(buf)
return errors.Wrap(err,"write failed")
}
- 在你的应用代码中,可以使用 errors.New 或者 errors.Errorf 返回错误
- 如果和其他库进行协作,考虑使用 errors.Wrap 或者 errors.Wrapf保存堆栈信息。同样适用于和标准库协作的时候
- 直接返回错误,而不是每个地方都到处打日志
在程序的顶部或者工作的goruoutine(请求入口),使用 %+v 把堆栈详情打出来!
go1.13之后的error
go1.13为 errors 和 fmt 标准库包引入了新特性,以简化处理包含其他错误的错误。其中最重要的是: 包含另一个错误的 error 可以实现返回底层错误的 Unwrap 方法。如果 e1.Unwrap() 返回 e2,那么我们说 e1 包装 e2,您可以展开 e1 以获得 e2。
按照此约定,我们可以为上面的 QueryError 类型指定一个 Unwrap 方法,该方法返回其包含的错误:
go1.13 errors 包包含两个用于检查错误的新函数:Is 和 As。