Go编写好的错误处理

编写好的错误处理

Go的错误机制:

  • 没有异常机制

  • error 类型实现了 error 接口

    type error interface {
      Error() string
    }
    
  • 可以通过 errors.News 来快速创建错误实例

    errors.News("n must be in the range [0,10]")
    

拿Fibonacci举例:

func GetFibonacci(n int) []int {
    fibList := []int{1, 2}

    for i := 2; i < n; i++ {
        fibList = append(fibList, fibList[i-2]+fibList[i-1])
    }
    return fibList
}

func TestGetFibonacci(t *testing.T) {
    t.Log(GetFibonacci(10))
    t.Log(GetFibonacci(-10))
    /** 运行结果
    === RUN   TestGetFibonacci
        TestGetFibonacci: err_test.go:15: [1 2 3 5 8 13 21 34 55 89]
        TestGetFibonacci: err_test.go:21: [1 2]
    --- PASS: TestGetFibonacci (0.00s)
    */
}

可以看到没有对入参进行校验

现在做下校验:

func GetFibonacci(n int) ([]int, error) {
    if n < 2 || n > 100 {
        return nil, errors.New("n should be in [2,100]")
    }
    fibList := []int{1, 2}

    for i := 2; i < n; i++ {
        fibList = append(fibList, fibList[i-2]+fibList[i-1])
    }
    return fibList, nil
}

func TestGetFibonacci(t *testing.T) {
    // 如果有错误进行错误输出
    if v, err := GetFibonacci(-10); err != nil {
        t.Error(err)
    } else {
        t.Log(v)
    }
    /** 运行结果
    === RUN   TestGetFibonacci
        TestGetFibonacci: err_test.go:22: n should be in [2,100]
    --- FAIL: TestGetFibonacci (0.00s)
    */
}

假设现在有个需求,返回的值是太小了还是太大了,返回不同的错误,最简单的方法直接改造GetFibonacci

func GetFibonacci(n int) ([]int, error) {
    if n < 2 {
        return nil, errors.New("n should be not less than 2")
    }

    if n > 100 {
        return nil, errors.New("n should be not larger than 100")
    }

    fibList := []int{1, 2}

    for i := 2; i < n; i++ {
        fibList = append(fibList, fibList[i-2]+fibList[i-1])
    }
    return fibList, nil
}

如果区分错误类型,依靠字符串去匹配简直太麻烦还容易出错,最常见的解决方法,定义两个预置的错误:

var LessThanTwoError = errors.New("n should be not less than 2")
var LargerThenHundredError = errors.New("n should be not larger than 100")

func GetFibonacci(n int) ([]int, error) {
    if n < 2 {
        return nil, LessThanTwoError
    }

    if n > 100 {
        return nil, LargerThenHundredError
    }

    fibList := []int{1, 2}

    for i := 2; i < n; i++ {
        fibList = append(fibList, fibList[i-2]+fibList[i-1])
    }
    return fibList, nil
}

func TestGetFibonacci(t *testing.T) {
    // 如果有错误进行错误输出
    if v, err := GetFibonacci(-10); err != nil {
        // 假如调用者需要判断错误的就比较简单了
        if err == LessThanTwoError {
            fmt.Println("It is less.")
        }
        t.Error(err)
    } else {
        t.Log(v)
    }
    /** 运行结果
    === RUN   TestGetFibonacci
    It is less.
        TestGetFibonacci: err_test.go:36: n should be not less than 2
    --- FAIL: TestGetFibonacci (0.00s)
    */
}

总结:

  • 定义不同的错误变量,以便于判断错误类型

  • 及早失败,避免嵌套,提高代码可读性

panic和recover

panic

panic:

  • panic 用于不可以恢复的错误
  • panic 退出前会执行 defer 指定的内容

panic vs. os.Exit:

  • os.Exit 退出时不会调用 defer 指定的函数
  • os.Exit 退出时不输出当前调用栈的信息
func TestExit(t *testing.T) {
    fmt.Println("Start")
    os.Exit(-1)
    /** 运行结果
    === RUN   TestExit
    Start

    Process finished with exit code 1
    */
}

func TestPanic(t *testing.T) {
    defer func() {
        fmt.Println("Finally!")
    }()
    fmt.Println("Start")
    panic(errors.New("Something wrong!"))
    /** 运行结果:
    === RUN   TestPanic
    Start
    Finally!
    --- FAIL: TestPanic (0.00s)
    panic: Something wrong! [recovered]
        panic: Something wrong!

    goroutine 6 [running]:
    testing.tRunner.func1.1(0x1119860, 0xc000046510)
        /usr/local/Cellar/go/1.14.2_1/libexec/src/testing/testing.go:940 +0x2f5
    testing.tRunner.func1(0xc00011a120)
        /usr/local/Cellar/go/1.14.2_1/libexec/src/testing/testing.go:943 +0x3f9
    panic(0x1119860, 0xc000046510)
        /usr/local/Cellar/go/1.14.2_1/libexec/src/runtime/panic.go:969 +0x166
    command-line-arguments.TestPanic(0xc00011a120)
        /Users/gaobinzhan/Documents/Go/learning/src/test/err_test.go:65 +0xd7
    testing.tRunner(0xc00011a120, 0x114afa0)
        /usr/local/Cellar/go/1.14.2_1/libexec/src/testing/testing.go:991 +0xdc
    created by testing.(*T).Run
        /usr/local/Cellar/go/1.14.2_1/libexec/src/testing/testing.go:1042 +0x357

    Process finished with exit code 1
    */
}

recover

大家在写c++或者php代码的时候,总有一种习惯不希望这个程序被中断或者退出,用来捕获。

php代码:

try {

} catch (\Throwable $throwable) {
    
}

c++ 代码:

try{
  ...
}catch(...){
  
}

go代码:

defer func(){
  if err := recover(); err != nil {
    // 恢复错误
  }
}()
func TestRecover(t *testing.T) {
    defer func() {
        if err := recover(); err != nil {
            // 没有写错误恢复 只是打印出来了
            fmt.Println("recovered from", err)
        }
    }()
    fmt.Println("Start")
    panic(errors.New("Something wrong!"))
    /** 运行结果:
    === RUN   TestRecover
    Start
    recovered from Something wrong!
    --- PASS: TestRecover (0.00s)
    */
}

最常见的"错误恢复":

defer func() {
  if err := recover(); err != nil {
    log.Error("recovered panic",err)
  }
}()

当心!recover 成为恶魔:

  • 形成僵尸服务进程,导致 health check 失效。
  • “Let it Crash!” 往往是我们恢复不确定性错误的最好方法。

就如上常见的“错误恢复”只是记录了一下,这样的恢复方式是非常危险的。

一定要当心我们自己 recover 在做的事,因为我们 recover 的时候并不去检测错误到底发生了什么错误,而是简单的记录了一下或者忽略。

这时候可能是系统里面的某些核心资源已经消耗完了,我们这样把它强制恢复掉,其实系统依然不能够正常地工作的,还是导致我们的一些健康检查程序 health check 没有办法检查出当前系统的问题。

因为很多的这种 health check 只是检查当前的系统进程在还是不在,因为我们的进程是在的,所以就会导致一种僵尸服务进程,它好像活着,但它也不能提供服务。

这种情况下个人认为倒不如采用一种可恢复的设计模式其中的一种叫 Let it Crash ,干脆 Crash掉,一旦Crash掉 守护进程 ,就会帮我们的服务进程重新提起来。

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