【Go】如何正确理解error

被误解的error,被滥用的异常

Go error从诞生之初就招人诟病,人们总是调侃写Go代码有一半都是if err != error {}。但其实大家都没有正确的去理解和看待error这个东西,这也是我写本篇文章的目的。

其实大家诟病Go error的核心矛盾在于error不能自动结束函数流程。我们期望有一种机制能在err != nil的时候自动返回,而不是手动return。你之所以会这样想是因为你习惯了使用异常,你希望向使用异常那样使用error。但是在Go语言中,异常和error显然是两个有着明显区别的概念。我们似乎很少去思考error和异常之间的区别,甚至很多人习惯了无脑抛异常,导致异常被滥用。

在Go语言中,error是一个值,一个很普通的值,和2,"abc"没有任何区别的值,而且还是可以被忽略的。它仅仅表示一种函数执行后的状态,没有任何特殊,Go语言希望我们手动的去处理它,决定程序的流程是返回还是继续,决定是忽略error还是返回它。从某种意义上来说,error应该是一种正常状态,异常才是不正常的状态,程序必须马上终止执行。

Go语言的error是一个值!实际上任何语言的error都是值,不同的是对error的处理机制。Go是一门支持多返回值的语言,这使得它可以将真正的返回值和error分开返回,这同样也使得error的处理很难自动化进行,我们必须在err != nil的时候手动结束函数流程。同样,Go语言的error也是非常灵活和自然的。你会发现在Go语言中处理error不需要任何额外的语法,而且不需要扩展if的语义。至于错误应该何时传播,何时终止传播,何时处理,这并不是Go语言的问题,而是所有语言的问题。

其他语言对error的处理

Rust

鼎鼎大名,它的核心思想是如果一片内存在任何时候都只被一个变量引用,那么我们就可以在变量退出作用域时安全的释放这片内存。所有权和借用的概念都是从这一思想来的。《Rust程序语言设计》的错误处理一章有一段话,大家可以好好理解以下,它说的就是error和异常。

Rust 将错误分为两大类:可恢复的recoverable)和 不可恢复的unrecoverable)错误。对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。

Rust对错误的处理方式是将返回值和error用枚举类型结合起来。

enum Result<T, E> {
    Ok(T),
    Err(E),
}

使用?传递错误:

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}

使用match手动处理错误:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

Rust提供了很多错误处理相关的函数,这里仅举两个基本的例子。

V

V语言很像是Go和Rust的合体。它算是一门比较年轻的语言,目前最新的是版本是0.3。它的吉祥物总感觉很熟悉,但又叫不上名字。

在Go语言中,我们通常会返回一个值和一个error,比如:

func string2int(s string) (int, error) {
  ...
}

而V语言则将两者结合,形成了一种可选类型,就是在具体类型前加上一个?,上面的函数在V语言中会有如下定义:

fn string2int(s string) ?int {
  return error("未实现")
}

对于?Type这样的类型,我们可以直接返回Type类型的值,也可以直接返回error,但是不支持多返回值。

如果在函数调用后面加上一个?,那么如果函数返回error,error就会向上传递,知道传递到main函数,程序就会panic。

import net.http

fn f(url string) ?string {
  resp := http.get(url)?
  return resp.text
}

如果需要手动处理error,可以在函数调用后加上一个or代码块。在or代码块中,既可以结束程序,也可以给一个默认值,在or代码块中,可以直接访问err变量。

结束流程的例子:

user := repo.find_user_by_id(7) or {
  println(err) // "User 7 not found"
  return
}

提供默认值的例子:

fn do_something(s string) ?string {
  if s == 'foo' {
    return 'foo'
  }
  return error('invalid string') // Could be `return none` as well
}

a := do_something('foo') or { 'default' } // a will be 'foo'
b := do_something('bar') or { 'default' } // b will be 'default'
println(a)
println(b)

其实我们可以发现,使用?调用函数和or {return err}是等价的。

此外,还也可以使用if-else处理可选值:

import net.http

if resp := http.get('https://google.com') {
  println(resp.text) // resp is a http.Response, not an optional
} else {
  println(err)
}

仔细对比就会发现,这不就是Go语言中if v,err := xxx(); err != nil {} else {}的简化形式吗。

V语言中error也是值,但是它提供了更自动化的机制来处理error,使得error有了一点异常的影子。

Zig

Zig可能听过的人比较少,但它是励志想要替换C语言的一门语言。它的宣言是专注于调试你的程序,而不是编程语言知识。

Zig也提供了一种机制将返回值和error组合在一起,称为错误联合类型(Error Union Type)。其语法是errorSetType!Type,其中errorSetType可以省略,也就是说可以简写为!Type

pub fn parseU64(buf: []const u8, radix: u8) !u64 {
  ...
}

能够处理错误联合类型的有catchtryswitcherrdefer

使用cache提供默认值:

fn doAThing(str: []u8) void {
    const number = parseU64(str, 10) catch 13;
    ...
}

使用try传递error:

fn doAThing(str: []u8) !void {
    const number = try parseU64(str, 10);
    ...
}

它实际上等价于下面的代码:

fn doAThing(str: []u8) !void {
    const number = parseU64(str, 10) catch |err| return err;
    ...
}

使用switch处理不同错误,奇奇怪怪的语法🤣:

fn doAThing(str: []u8) void {
    if (parseU64(str, 10)) |number| {
        doSomethingWithNumber(number);
    } else |err| switch (err) {
        error.Overflow => {
            // handle overflow...
        },
        // we promise that InvalidChar won't happen (or crash in debug mode if it does)
        error.InvalidChar => unreachable,
    }
}

errdefer,发生错误时的延迟执行:

fn createFoo(param: i32) !Foo {
    const foo = try tryToAllocateFoo();
    // now we have allocated foo. we need to free it if the function fails.
    // but we want to return it if the function succeeds.
    errdefer deallocateFoo(foo);

    const tmp_buf = allocateTmpBuffer() orelse return error.OutOfMemory;
    // tmp_buf is truly a temporary resource, and we for sure want to clean it up
    // before this block leaves scope
    defer deallocateTmpBuffer(tmp_buf);

    if (param > 1337) return error.InvalidParam;

    // here the errdefer will not run since we're returning success from the function.
    // but the defer will run!
    return foo;
}

Zig不支持多返回值,返回值和error的组合算是一个不错的选择,而且提供了一些自动化的机制,但是你需要明白什么时候会结束程序流程,什么时候不会。

总结

Go语言error的设计是不是最优秀的?我认为不是,但也不足以让我诟病它。当你学过几门编程语言之后就会深刻体会到Go是多么的简洁明了,它既没有奇奇怪怪的语法,也没有大篇大篇的关键字,绝大多数表述都是很自然的,它与我们的经验相符,因此,我很容易接受它。试想以下,我买个锤子,结果你给我一本两百页的说明书,并叮嘱我看完说明书以后才能使用,我一定会脱口而出WTF。对于习惯了996的我们,恨不得一行代码跑出10行的功能,有没有一种可能是我们真的太卷了呢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值