Go入门:理解Go的Error处理

在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什么时候用比较合适?

  1. 在服务初始化失败时,一些main函数里面的强依赖的基础组建,例如:mysql/redis/kafka/mq等,当连接错误时,必须panic。
  2. 在代码中读取配置文件出错时,例如:读取配置中心出错,读取Apollo出错,这些基础配置的读取,如果出错,一定要panic,因为是强依赖,会影响后续的程序正常运行。
  3. 在go语言中,也有异常捕获(recover)机制, 在对于弱依赖出错时,可以recover.
    4 例如不可恢复的错误,例如:索引越界,不可恢复的环境问题,内存溢出,都推荐使用panic来让程序直接退出。


go的sentinel Error

这个单词也可以叫:哨兵error.
先说下结论: 不建议使用sentinel Error


sentinel是什么?
对于预定义的特定错误,我们叫它为 sentinel error (哨兵Error),这个名字来源于计算机编程中使用一个特定值来表示不可能进行进一步处理的做法。
对于Sentienl Error的例子可以参考 io.EOF,如下图:

image.png

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



为什么不推荐使用 sentinel?

  1. Sentinel errors 会成为你 API 公共部分。
  2. 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 他提供了底层执行了什么操作、那个路径出了什么问题。

image.png

结论: 调用者要使用类型断言和类型 switch,就要让自定义的 error 变为 public。这种模型会导致和调用者产生强耦合,从而导致 API 变得脆弱。
结论是尽量避免使用 error types,虽然错误类型比 sentinel errors 更好,因为它们可以捕获关于出错的更多上下文,但是 error types 共享 error values 许多相同的问题。
因此,我的建议是避免错误类型,或者至少避免将它们作为公共 API 的一部分。


Handling Error

  1. 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 方法,该方法返回其包含的错误:

image.png

go1.13 errors 包包含两个用于检查错误的新函数:Is 和 As。

image.png
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容