文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

我撰写本文,是为了回应别处一个文章的说法。具体来说,就是错误处理的方式太“不优雅”。

go语言的错误处理方式,往往被新接触go语言的新人所诟病。注意哦,我说的可不是“异常处理”。Go语言中是没有异常的,尽管你会说Go语言不是使用panic和recover来处理异常的么。但是,我必须明确指出,我是反对你这种说法的。经常使用这种说法,其实是反设计模式的。你的程序中出现某种错误时,Go只是使用一种类似这种处理方式去处理罢了。

Go的作者们想了很多种处理error的方案。我想他们肯定是权衡再三,从中选取了一个相对最好的解决方案,使其简单而又优雅。

我们知道,GO是可以返回多值结果的。按照惯例,如果有错误发生,函数将返回一个错误,并且该错误是作为最后一个返回值返回的。

func ETPhoneHome(msg string) (string, error) {
    // implementation
}

一个error 类型如下:

type error interface {
    Error() string
}

这意味着,任何实现这个将string作为返回值的Error()方法的类型,都实现了error 接口。string返回值可以描述错误发生的详情。如果觉得对此有所疑惑的话,可以回顾下Go接口的相关内容

在很多Go的代码中,你会见到类似如下的代码段:

response, err := ETPhoneHome("I can't fly this bike forever!")
if err != nil {
    // handle the error, often:
    return err
}
// do something with response
第 1 段(可获 2.19 积分)

对于此处貌似是常量的nil的校验,许多编程的哥们感觉有点儿讶异。

if err != nil {
    // handle the error
}

我们不妨先跳出Go语言本身,去和其他编程语言做些比较。

下面的代码,给出了一个类似的java语言编写的类:

// Java
try {
    String response = ET.phoneHome("I hate white lab coats.")
    System.out.println(response);
} catch(Exception e) {
    System.out.println("Exception thrown  :" + e);
}

下面是 Swift 2 编写的示例:

// Swift
// 方式1
do {
    let response = try ETPhoneHome("Those are guns not walkie talkies!")
    // process response
} catch (let error as NSError) {
    print("oops", error)
}

// 方式2: 给一个可选变量赋值
let response = try? ETPhoneHome("Halp!")

// 方式3: 当抛出error时,应用程序直接运行出错
let response = try! ETPhoneHome("BOOM")

哪种方式更简洁明了? 是Do, try, catch 这种方式?还是选择从多个error中catch可选变量的这种方式 ? 或者说,更简洁的还是下面Go这种方式:

// Go
if err != nil {
    // handle the error
}

当然,你可以狡辩会所,只需一个调用者使用 try/catch就可以了。方法调用的各种下游,时时刻刻都会抛出各种异常。然而,实际场景汇中,其实很少会是这样的。使用swift编程时,我们还需要自己在究竟是选择do/catch的方式,还是  try 方式?其实,在单元测试用例中,确实得经常做类似的选择。

中断式的语言,例如Ruby,相比之下显得更为简洁,因为它根本不需要编程人员去处理任何异常。Swift和Java在强制调用者处理异常之前,它们将不会编译。GO语言则是,除非调用者处理或忽略错误值,否则将不会编译

 

第 2 段(可获 1.73 积分)

Ruby中,你只需知道某个方法可能会导致异常。在方法名中使用(ex: model.save!) ,这样的标识标明该方法可能会抛出某种异常。但是除此之外,异常纠结什么时候发生,发生在什么具体位置,具体发生的是设么用的异常,这些都只能听天由命的靠运气碰碰了。因此,在可中断的开发语言中,在运行时使得异常可见,这种方式显得更为普遍。Go(与Swift和Java一样)会尝试告诉你可能的的所有错误情况,以限制运行时发生意外的行为。

Go作者认为,并不是所有的异常都是例外。不是所有的错误都应该使你的应用程序崩溃。我当然同意,如果您可以从错误中正常恢复,您应该这样做。您的应用程序将稳定而健壮。当然,说起来容易,做起来难。这需要,程序员自己去做更多的努力。在Ruby中,你可以确保一个异常永远不会发生,继续前进。也就是说,直到客户打电话给你抱怨你的应用程序出问题了,你才会有所察觉到发生了异常。

第 3 段(可获 1.99 积分)

Errors是值类型

Go团队有意将errors作为值类型。

值类型是可编程的。由于errors是值类型,所以可以对errors进行编程。errors不像异常(exceptions)。它们没有什么特别的,仅仅是未处理的异常可能会导致程序崩溃而已。

上述相关内容出自 Rob Pike(https://blog.golang.org/errors-are-values)。带着开森的心情接着去学习Pike的博客内容吧。

Go中,向很多传递其他类型值一样,将errors传递出去。因为,本质上来讲它们都是某种类型而已。

换句话说,异常(Exceptions)有些特有的特点。如果放认异常,不加处理的话,会导致你的程序挂掉。(Swift语言中,errors和exceptions从技术上来说是有差异的。但是,单从errors处理方面看的话,两者的语意语法几乎是一样的)

所以说,如果我们用简单明了的值类型就能对errors做各种处理,干嘛还脱裤子放屁多此一举的去使用特殊的exception 这个东东呢?该从编程语言层面,将这种复杂的东西一脚踹出去了,不是么?

当然,我不能确定GO的开发者们得出的结论,是否和我的想法一致,但从我个人角度来讲,却真是如此的。

第 4 段(可获 2.11 积分)

流程控制

Rob Pike 还写道:

Go语言中的errors处理,不会“遮蔽”流程控制。

因为,在Go中,要么1)error立马被处理或者被忽略,要么2)error会被返回给调用者处,从而你可以跟踪error的路径信息。

然而,在很多其他语言中,流程控制却并不会向上述所说的这样“清晰”。以swift为例:

// Swift
do {
    // If successful, the happy path is now nested.
} catch (let error as NSError) {
    // handle error
}
第 5 段(可获 0.71 积分)

通常情况下,直接使用嵌套就搞定了。但是,对于有额外的 if/else的场景,又该如何处理呢?这个时候,代码可读性会大大的降低。

然而,Go中使用如下方式:

// Go
result, err := SomeFunction()
if err != nil {
    // handle the error
}
// 令人哈皮的是,就像通常的处理方式一样,不需要任何恶心的嵌套处理

Swift 2引入guard 声明去改善流程控制。但是,你只能按照下述方式去使用它 :

// Swift
guard let result = try? SomeFunction() else {
    return
}
// result is in scope, proceed with happy path

可见,你不能使用swift的guard去对 error做进一步的处理。但是,固执的你说,我就是想对errof做进一步的处理,而且还不适用嵌套,怎么弄?Swift语言中,你可以使用如下方式 :

// Swift
var result: Any?
do {
    result = SomeFunction()
} catch (let error as NSError) {
    // handle error
}
// proceed with happy path

上面的处理方式,有下面这样更清爽明了么?

// Go
result, err := SomeFunction()
if err != nil {
    // handle the error
}
// trot down the happy path

Ruby中,异常的抛出与捕获,理解起来就像是直接使用了恶心的“GOGO”声明。抛出异常的Ruby代码可能调用链很深。调用者捕获的异常,可能发生在深深的调用链的任何位置发生。所以说,异常处理就像是使用“GOTO”直接到达堆栈的任何随机位置一样。这种流程控制无疑是很糟糕的。Ruby代码出异常时,我经常需要苦逼的去花费n多时间调试。这真是累死人的节奏,难调试的一B。

第 6 段(可获 2.03 积分)

按自己的方式来编码

你可以使用下面这种方式处理error

if err != nil {
    // handle the error
}

但这并意味着这样的代码就应该写的到处都是。你可以尽情的按照自己的方式来。将error处理代理给某个函数或者对象。毕竟,你可是个牛叉叉的编程高手,对吧?写个抽象搞定它。

例如,我在用GO语言编写命令行工具时,经常使用下面这个函数:

func checkErr(err error) {
    if err != nil {
        fmt.Println("ERROR:", err)
        os.Exit(1)
    }
}

// 或者作为一个常用的用户对重新编码:
func checkErr(err error) {
    if err != nil {
        log.Fatal("ERROR:", err)
    }
}

命令行工具汇总,它效果不错,但是,在web服务端上,可就不咋地咯。

Rob Pike建议对于使用帮助类的函数,最好就不要这样使用了。好好的拜读一下Pike的博客(https://blog.golang.org/errors-are-values),对于如何避免大量的error处理,该博客中给出了大量的实例。

第 7 段(可获 1.45 积分)

结论

希望我已经说清楚了,GO的error处理方式并不那么糟糕。我个人,其实倒是觉得Go的处理方式蛮优雅的。个人认为,很多刚接触GO的“新人”,之所以一脸懵逼,是因为Go很多标准库都会有error返回值。这就意味着,你不得不去处理这些error。理所当然,它就需要程序员为了error,花费费额外的精力。但是,从另一方面来说,这也会使得程序员更能将软件写的更加健壮、更加稳定。这其实不是坏事嘛,对吧?

更新

关于Reddit和Counter,当前有各种探讨。

像我这样,本身之前从事后端动态语言的编程人员,慢慢的就将诶和搜了Go的error处理方式。但是,之前使用Haskell等类似开发语言的同学,可能往往则是持相反的观点。

第 8 段(可获 1.38 积分)

文章评论