从错误处理看 Rust 的语言和 Go 语言的设计

golang 对比

先用go语言做一个对比,比如我们要将两个字符串转换为数字后返回结果,如果使用golang,则实现方式如下:

func multiply(first_number string, second_number string) (result int, err error) {
    var (
        first  int
        second int
    )
    first, err = strconv.Atoi(first_number)
    if err != nil {
        return
    }

    second, err = strconv.Atoi(second_number)
    if err != nil {
        return
    }
    result = first * second
    return
}

可以看到,这也是go语言最常用的错误处理机制,如果捕获到错误,则将错误返回,否则返回正常的结果。这样处理错误的结果就是:每一个错误都要进行人肉处理,怎么很多的冗余性代码,如果代码量很大的话,会写很多冗余的代码,很不优雅。

rust 实现

基础版本

现在,使用Rust进行实现同样的逻辑:

fn multiply(first_number_str: &str, second_number_str: &str) -> i32 {
    let first_number = first_number_str.parse::<i32>().unwrap();
    let second_number = second_number_str.parse::<i32>().unwrap();
    first_number * second_number
}

以上代码相比golang的代码,并没有多少现金的地方。因为程序在解析到程序异常的时候,直接终止程序。这样在项目开发中是很不友好的,但是,这个只是最粗糙的实现方式,我们使用match进行优化它:

match

use std::num::ParseIntError;
type AliasResult<T> = Result<T, ParseIntError>;

#[allow(dead_code)]
fn multiply1(first_number_str: &str, second_number_str: &str) -> AliasResult<i32> {
    match first_number_str.parse::<i32>() {
        Err(e) => Err(e),
        Ok(first_number) => match second_number_str.parse::<i32>() {
            Err(e) => Err(e),
            Ok(second_number) => Ok(first_number * second_number)
        }
    }
}

这个和golang相比,实现不相上下。都是检测到异常就返回异常,如果止步于此,那么,rust只能和golang打成平手。但是,rust还提供的函数式的结果处理机制:

使用 map 和 and_then 实现

#[allow(dead_code)]
fn multiply2(first_number_str: &str, second_number_str: &str) -> AliasResult<i32> {
    first_number_str.parse::<i32>().and_then(|first_number| {
        second_number_str.parse::<i32>().map(|second_number| first_number * second_number)
    })
}

可以看到,通过算子的方式,rust可以通过两行代码就可以处理想要的结果,这里已经是相对比较好的实现了。但是,相比之前的代码,不过不了解函数式编程,可能相对难懂。那么我们有没有折中的实现呢

通用折中的实现

#[allow(dead_code)]
fn multiply3(first_number_str: &str, second_number_str: &str) -> AliasResult<i32> {
    let first_number = match first_number_str.parse::<i32>() {
        Ok(first_number) => first_number,
        Err(e) => return Err(e)
    };

    let second_number = match second_number_str.parse::<i32>() {
        Ok(second_number) => second_number,
        Err(e) => return Err(e)
    };

    Ok(first_number * second_number)
}

以上实现,相对来说清晰易读,但是,有没有一种方式,能将重复的错误处理简化掉呢,答案是有:

#[allow(dead_code)]
fn multiply4(first_number_str: &str, second_number_str: &str) -> AliasResult<i32> {
    let first_number = first_number_str.parse::<i32>()?;
    let second_number = second_number_str.parse::<i32>()?;

    Ok(first_number * second_number)
}

以上实现,相比之前第一个最粗糙的实现,只是多了两个?.但是,他依然做了最安全的错误处理,同时,相比于golang,代码量成倍的减少,并且代码的易读性也相当的高,同时,相比代码的执行效率,rust也是完胜golang的。所以,拥抱rust吧~~

王举
讨论数量: 6
chongyi

Rust 本质上也可以实现 Golang 的错误处理方式,但是基于 Enum 的实现可以更为优雅,更多的是鉴于 match 这个模式匹配提供的强大解构能力

5个月前 评论
王举

@chongyi 嗯,所以感觉golang错误处理还是太原始了,人肉处理错误比较难受 :relieved:

5个月前 评论
chongyi

@王举 这个是理念问题,Golang 语言特性少,上手速度快,当然带来的结果就是后期维护在处理上就需要花更多心思处理这些边界问题。

Rust 细节多,掌握难度大(其实也还好,不用全部掌握的时候也可以开始写,反正编译器会教你 :joy:),但是维护起来不需要脑子,哈哈

5个月前 评论
GhostCoder

Rust 有先苦后甜的感觉?

4个月前 评论

都是大佬,不敢说话,会的语言真多

1个月前 评论

学了 Haskell 之后 发觉 其实 Rust 就类似抽象了一个 ADT 来做错误处理

1个月前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!