介绍

follow

blog

flow about tech blog.

Rust

video

Rust

细说Rust错误处理

handle-error.png

原文地址:https://github.com/baoyachi/rust-error-handle

1. 前言

这篇文章写得比较长,全文读完大约需要15-20min,如果对Rust的错误处理不清楚或还有些许模糊的同学,请静下心来细细阅读。当读完该篇文章后,可以说对Rust的错误处理可以做到掌握自如。

笔者花费较长篇幅来描述错误处理的来去,详细介绍其及一步步梳理内容,望大家能耐心读完后对大家有所帮助。当然,在写这篇文章之时,也借阅了大量互联网资料,详见链接见底部参考链接

掌握好Rust的错误设计,不仅可以提升我们对错误处理的认识,对代码结构、层次都有很大的帮助。那废话不多说,那我们开启这段阅读之旅吧😄!

2. 背景

笔者在写这篇文章时,也翻阅一些资料关于Rust的错误处理资料,多数是对其一笔带过,导致之前接触过其他语言的新同学来说,上手处理Rust的错误会有当头棒喝的感觉。找些资料发现unwrap()也可以解决问题,然后心中暗自窃喜,程序在运行过程中,因为忽略检查或程序逻辑判断,导致某些情况,程序panic。这可能是我们最不愿看到的现象,遂又回到起点,重新去了解Rust的错误处理。

这篇文章,通过一步步介绍,让大家清晰知道Rust的错误处理的究竟。介绍在Rust中的错误使用及如何处理错误,以及在实际工作中关于其使用技巧。

3. unwrap的危害!

下面我们来看一段代码,执行一下:

fn main() {
    let path = "/tmp/dat";
    println!("{}", read_file(path));
}

fn read_file(path: &str) -> String {
    std::fs::read_to_string(path).unwrap()
}

程序执行结果:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:1188:5
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/libunwind.rs:88
  ...
  15: rust_sugar::read_file
             at src/main.rs:7
  16: rust_sugar::main
             at src/main.rs:3
  ...
  25: rust_sugar::read_file
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

什么,因为path路径不对,程序竟然崩溃了,这个是我们不能接受的!

unwrap() 这个操作在rust代码中,应该看过很多这种代码,甚至此时我们正在使用它。它主要用于OptionResult的打开其包装的结果。常常我们在代码中,使用简单,或快速处理,使用了 unwrap() 的操作,但是,它是一个非常危险的信号!

可能因为没有程序检查或校验,潜在的bug可能就出现其中,使得我们程序往往就panic了。这可能使我们最不愿看到的现象。

在实际项目开发中,程序中可能充斥着大量代码,我们很难避免unwrap()的出现,为了解决这种问题,我们通过做code review,或使用脚本工具检查其降低其出现的可能性。

通常每个项目都有一些约束,或许:在大型项目开发中, 不用unwrap() 方法,使用其他方式处理程序,unwrap() 的不出现可能会使得程序的健壮性高出很多。

这里前提是团队或大型项目,如果只是写一个简单例子(demo)就不在本篇文章的讨论范畴。因为一个Demo的问题,可能只是快速示范或演示,不考虑程序健壮性, unwrap() 的操作可能会更方便代码表达。

可能有人会问,我们通常跑程序unit test,其中的很多mock数据会有 unwrap() 的操作,我们只是为了在单元测试中使得程序简单。这种也能不使用吗?答案:是的,完全可以不使用 unwrap() 也可以做到的。

4. 对比语言处理错误

说到unwrap(),我们不得不提到rust的错误处理,unwrap()Rust的错误处理是密不可分的。

4.1 golang的错误处理演示

如果了解golang的话,应该清楚下面这段代码的意思:

package main

import (
    "io/ioutil"
    "log"
)

func main() {
    path := "/tmp/dat"  //文件路径
    file, err := readFile(path) 
    if err != nil {
        log.Fatal(err) //错误打印
    }
    println("%s", file) //打印文件内容
}

func readFile(path string) (string, error) {
    dat, err := ioutil.ReadFile(path)  //读取文件内容
    if err != nil {  //判断err是否为nil
        return "", err  //不为nil,返回err结果
    }
    return string(dat), nil  //err=nil,返回读取文件内容
}

我们执行下程序,打印如下。执行错误,当然,因为我们给的文件路径不存在,程序报错。

2020/02/24 01:24:04 open /tmp/dat: no such file or directory

这里,golang采用多返回值方式,程序报错返回错误问题,通过判断 err!=nil 来决定程序是否继续执行或终止该逻辑。当然,如果接触过golang项目时,会发现程序中大量充斥着if err!=nil的代码,对此网上有对if err!=nil进行了很多讨论,因为这个不在本篇文章的范畴中,在此不对其追溯、讨论。

4.2 Rust 错误处理示例

对比了golang代码,我们对照上面的例子,看下在Rust中如何编写这段程序,代码如下:

fn main() {
    let path = "/tmp/dat";  //文件路径
    match read_file(path) { //判断方法结果
        Ok(file) => { println!("{}", file) } //OK 代表读取到文件内容,正确打印文件内容
        Err(e) => { println!("{} {}", path, e) } //Err代表结果不存在,打印错误结果
    }
}

fn read_file(path: &str) -> Result<String,std::io::Error> { //Result作为结果返回值
    std::fs::read_to_string(path) //读取文件内容
}

当前,因为我们给的文件路径不存在,程序报错,打印内容如下:

No such file or directory (os error 2)

Rust代表中,Result是一个enum枚举对象,部分源码如下:


#![allow(unused)]
fn main() {
pub enum Result<T, E> {
    /// Contains the success value
    Ok(#[stable(feature = "rust1", since = "1.0.0")] T),

    /// Contains the error value
    Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}
}

通常我们使用Result的枚举对象作为程序的返回值,通过Result来判断其结果,我们使用match匹配的方式来获取Result的内容,判断正常(Ok)或错误(Err)。

或许,我们大致向上看去,golang代码和Rust代码没有本质区别,都是采用返回值方式,给出程序结果。下面我们就对比两种语言说说之间区别:

  • golang采用多返回值方式,我们在拿到目标结果时(上面是指文件内容file),需要首先对err判断是否为nil,并且我们在return时,需要给多返回值分别赋值,调用时需要对 if err!=nil 做结果判断。
  • Rust中采用Result的枚举对象做结果返回。枚举的好处是:多选一。因为Result的枚举类型为OkErr,使得我们每次在返回Result的结果时,要么是Ok,要么是Err。它不需要return结果同时给两个值赋值,这样的情况只会存在一种可能性: Ok or Err
  • golang的函数调用需要对 if err!=nil做结果判断,因为这段代码 判断是手动逻辑,往往我们可能因为疏忽,导致这段逻辑缺失,缺少校验。当然,我们在编写代码期间可以通过某些工具 lint 扫描出这种潜在bug。
  • Rustmatch判断是自动打开,当然你也可以选择忽略其中某一个枚举值,我们不在此说明。

可能有人发现,如果我有多个函数,需要多个函数的执行结果,这样需要match代码多次,代码会不会是一坨一坨,显得代码很臃肿,难看。是的,这个问题提出的的确是有这种问题,不过这个在后面我们讲解的时候,会通过程序语法糖避免多次match多次结果的问题,不过我们在此先不叙说,后面将有介绍。

5. Rust中的错误处理

前面不管是golang还是Rust采用return返回值方式,两者都是为了解决程序中错误处理的问题。好了,前面说了这么多,我们还是回归正题:Rust中是如何对错误进行处理的?

要想细致了解Rust的错误处理,我们需要了解std::error::Error,该trait的内部方法,部分代码如下: 参考链接:https://doc.rust-lang.org/std/error/trait.Error.html


#![allow(unused)]
fn main() {
pub trait Error: Debug + Display {

    fn description(&self) -> &str {
        "description() is deprecated; use Display"
    }

    #[rustc_deprecated(since = "1.33.0", reason = "replaced by Error::source, which can support \
                                                   downcasting")]

    fn cause(&self) -> Option<&dyn Error> {
        self.source()
    }

    fn source(&self) -> Option<&(dyn Error + 'static)> { None }

    #[doc(hidden)]
    fn type_id(&self, _: private::Internal) -> TypeId where Self: 'static {
        TypeId::of::<Self>()
    }

    #[unstable(feature = "backtrace", issue = "53487")]
    fn backtrace(&self) -> Option<&Backtrace> {
        None
    }
}
}
  • description()在文档介绍中,尽管使用它不会导致编译警告,但新代码应该实现impl Display ,新impl的可以省略,不用实现该方法, 要获取字符串形式的错误描述,请使用to_string()

  • cause()1.33.0被抛弃,取而代之使用source()方法,新impl的不用实现该方法。

  • source()此错误的低级源,如果内部有错误类型Err返回:Some(e),如果没有返回:None

    • 如果当前Error是低级别的Error,并没有子Error,需要返回None。介于其本身默认有返回值None,可以不覆盖该方法。
    • 如果当前Error包含子Error,需要返回子ErrorSome(err),需要覆盖该方法。
  • type_id()该方法被隐藏。

  • backtrace()返回发生此错误的堆栈追溯,因为标记unstable,在Ruststable版本不被使用。

  • 自定义的Error需要impl std::fmt::Debug的trait,当然我们只需要在默认对象上添加注解:#[derive(Debug)]即可。

总结一下,自定义一个error需要实现如下几步:

  • 手动实现impl std::fmt::Display的trait,并实现 fmt(...)方法。
  • 手动实现impl std::fmt::Debugtrait,一般直接添加注解即可:#[derive(Debug)]
  • 手动实现impl std::error::Errortrait,并根据自身error级别是否覆盖std::error::Error中的source()方法。

下面我们自己手动实现下Rust自定义错误:CustomError

use std::error::Error;

///自定义类型 Error,实现std::fmt::Debug的trait
#[derive(Debug)]
struct CustomError {
    err: ChildError,
}

///实现Display的trait,并实现fmt方法
impl std::fmt::Display for CustomError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "CustomError is here!")
    }
}

///实现Error的trait,因为有子Error:ChildError,需要覆盖source()方法,返回Some(err)
impl std::error::Error for CustomError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.err)
    }
}


///子类型 Error,实现std::fmt::Debug的trait
#[derive(Debug)]
struct ChildError;

///实现Display的trait,并实现fmt方法
impl std::fmt::Display for ChildError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ChildError is here!")
    }
}

///实现Error的trait,因为没有子Error,不需要覆盖source()方法
impl std::error::Error for ChildError {}

///构建一个Result的结果,返回自定义的error:CustomError
fn get_super_error() -> Result<(), CustomError> {
    Err(CustomError { err: ChildError })
}

fn main() {
    match get_super_error() {
        Err(e) => {
            println!("Error: {}", e);
            println!("Caused by: {}", e.source().unwrap());
        }
        _ => println!("No error"),
    }
}
  • ChildError为子类型Error,没有覆盖source()方法,空实现了std::error::Error
  • CustomError有子类型ChildError,覆盖source(),并返回了子类型Option值:Some(&self.err)

运行执行结果,显示如下:

Error: CustomError is here!
Caused by: ChildError is here!

至此,我们就了解了如何实现Rust自定义Error了。

6. 自定义Error转换:From

上面我们说到,函数返回Result的结果时,需要获取函数的返回值是成功(Ok)还是失败(Err),需要使用match匹配,我们看下多函数之间调用是如何解决这类问题的?假设我们有个场景:

  • 读取一文件
  • 将文件内容转化为UTF8格式
  • 将转换后格式内容转为u32的数字。

所以我们有了下面三个函数(省略部分代码):


#![allow(unused)]
fn main() {
...

///读取文件内容
fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
    std::str::from_utf8(v)
}

/// 转化为u32数字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
    v.parse::<u32>()
}
}

最终,我们得到u32的数字,对于该场景如何组织我们代码呢?

  • unwrap()直接打开三个方法,取出值。这种方式太暴力,并且会有bug,造成程序panic,不被采纳。
  • match匹配,如何返回OK,继续下一步,否则报错终止逻辑,那我们试试。

参考代码如下:

fn main() {
    let path = "./dat";
    match read_file(path) {
        Ok(v) => {
            match to_utf8(v.as_bytes()) {
                Ok(u) => {
                    match to_u32(u) {
                        Ok(t) => {
                            println!("num:{:?}", u);
                        }
                        Err(e) => {
                            println!("{} {}", path, e)
                        }
                    }
                }
                Err(e) => {
                    println!("{} {}", path, e)
                }
            }
        }
        Err(e) => {
            println!("{} {}", path, e)
        }
    }
}

///读取文件内容
fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
    std::str::from_utf8(v)
}

/// 转化为u32数字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
    v.parse::<u32>()
}

天啊,虽然是实现了上面场景的需求,但是代码犹如叠罗汉,程序结构越来越深啊,这个是我们没法接受的!match匹配导致程序如此不堪一击。那么有没有第三种方法呢?当然是有的:From转换。

前面我们说到如何自定义的Error,如何我们将上面三个error收纳到我们自定义的Error中,将它们三个Error变成自定义Error子Error,这样我们对外的Result统一返回自定义的Error。这样程序应该可以改变点什么,我们来试试吧。


#![allow(unused)]
fn main() {
#[derive(Debug)]
enum CustomError {
    ParseIntError(std::num::ParseIntError),
    Utf8Error(std::str::Utf8Error),
    IoError(std::io::Error),
}
impl std::error::Error for CustomError{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self {
            CustomError::IoError(ref e) => Some(e),
            CustomError::Utf8Error(ref e) => Some(e),
            CustomError::ParseIntError(ref e) => Some(e),
        }
    }
}

impl Display for CustomError{
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match &self {
            CustomError::IoError(ref e) => e.fmt(f),
            CustomError::Utf8Error(ref e) => e.fmt(f),
            CustomError::ParseIntError(ref e) => e.fmt(f),
        }
    }
}

impl From<ParseIntError> for CustomError {
    fn from(s: std::num::ParseIntError) -> Self {
        CustomError::ParseIntError(s)
    }
}

impl From<IoError> for CustomError {
    fn from(s: std::io::Error) -> Self {
        CustomError::IoError(s)
    }
}

impl From<Utf8Error> for CustomError {
    fn from(s: std::str::Utf8Error) -> Self {
        CustomError::Utf8Error(s)
    }
}
}
  • CustomError为我们实现的自定义Error
  • CustomError有三个子类型Error
  • CustomError分别实现了三个子类型Error From的trait,将其类型包装为自定义Error的子类型

好了,有了自定义的CustomError,那怎么使用呢? 我们看代码:

use std::io::Error as IoError;
use std::str::Utf8Error;
use std::num::ParseIntError;
use std::fmt::{Display, Formatter};


fn main() -> std::result::Result<(),CustomError>{
    let path = "./dat";
    let v = read_file(path)?;
    let x = to_utf8(v.as_bytes())?;
    let u = to_u32(x)?;
    println!("num:{:?}",u);
    Ok(())
}

///读取文件内容
fn read_file(path: &str) -> std::result::Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, std::str::Utf8Error> {
    std::str::from_utf8(v)
}

/// 转化为u32数字
fn to_u32(v: &str) -> std::result::Result<u32, std::num::ParseIntError> {
    v.parse::<u32>()
}


#[derive(Debug)]
enum CustomError {
    ParseIntError(std::num::ParseIntError),
    Utf8Error(std::str::Utf8Error),
    IoError(std::io::Error),
}
impl std::error::Error for CustomError{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self {
            CustomError::IoError(ref e) => Some(e),
            CustomError::Utf8Error(ref e) => Some(e),
            CustomError::ParseIntError(ref e) => Some(e),
        }
    }
}

impl Display for CustomError{
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match &self {
            CustomError::IoError(ref e) => e.fmt(f),
            CustomError::Utf8Error(ref e) => e.fmt(f),
            CustomError::ParseIntError(ref e) => e.fmt(f),
        }
    }
}

impl From<ParseIntError> for CustomError {
    fn from(s: std::num::ParseIntError) -> Self {
        CustomError::ParseIntError(s)
    }
}

impl From<IoError> for CustomError {
    fn from(s: std::io::Error) -> Self {
        CustomError::IoError(s)
    }
}

impl From<Utf8Error> for CustomError {
    fn from(s: std::str::Utf8Error) -> Self {
        CustomError::Utf8Error(s)
    }
}

其实我们主要关心的是这段代码:

fn main() -> Result<(),CustomError>{
    let path = "./dat";
    let v = read_file(path)?;
    let x = to_utf8(v.as_bytes())?;
    let u = to_u32(x)?;
    println!("num:{:?}",u);
    Ok(())
}

我们使用了?来替代原来的match匹配的方式。?使用问号作用在函数的结束,意思是:

  • 程序接受了一个Result<(),CustomError>自定义的错误类型。
  • 当前如果函数结果错误,程序自动抛出Err自身错误类型,并包含相关自己类型错误信息,因为我们做了From转换的操作,该函数的自身类型错误会通过实现的From操作自动转化为CustomError的自定义类型错误。
  • 当前如果函数结果正确,继续之后逻辑,直到程序结束。

这样,我们通过From?解决了之前match匹配代码层级深的问题,因为这种转换是无感知的,使得我们在处理好错误类型后,只需要关心我们的目标值即可,这样不需要显示对Err(e)的数据单独处理,使得我们在函数后添加?后,程序一切都是自动了。

还记得我们之前讨论在对比golang的错误处理时的:if err!=nil的逻辑了吗,这种因为用了?语法糖使得该段判断将不再存在。

另外,我们还注意到,Result的结果可以作用在main函数上,

  • 是的,Result的结果不仅能作用在main函数上
  • Result还可以作用在单元测试上,这就是我们文中刚开始提到的:因为有了Result的作用,使得我们在程序中几乎可以完全摒弃unwrap()的代码块,使得程序更轻,大大减少潜在问题,程序组织结构更加清晰。

下面这是作用在单元测试上的Result的代码:


#![allow(unused)]
fn main() {
...

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_num() -> std::result::Result<(), CustomError> {
        let path = "./dat";
        let v = read_file(path)?;
        let x = to_utf8(v.as_bytes())?;
        let u = to_u32(x)?;
        assert_eq!(u, 8);
        Ok(())
    }
}
}

7. 重命名Result

我们在实际项目中,会大量使用如上的Result结果,并且ResultErr类型是我们自定义错误,导致我们写程序时会显得非常啰嗦冗余


#![allow(unused)]
fn main() {
///读取文件内容
fn read_file(path: &str) -> std::result::Result<String, CustomError> {
    let val = std::fs::read_to_string(path)?;
    Ok(val)
}

/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, CustomError> {
    let x = std::str::from_utf8(v)?;
    Ok(x)
}

/// 转化为u32数字
fn to_u32(v: &str) -> std::result::Result<u32, CustomError> {
    let i = v.parse::<u32>()?;
    Ok(i)
}
}

我们的程序中,会大量充斥着这种模板代码Rust本身支持对类型自定义,使得我们只需要重命名Result即可:


#![allow(unused)]
fn main() {
pub type IResult<I> = std::result::Result<I, CustomError>; ///自定义Result类型:IResult
}

这样,凡是使用的是自定义类型错误的Result都可以使用IResult来替换std::result::Result的类型,使得简化程序,隐藏Error类型及细节,关注目标主体,代码如下:


#![allow(unused)]
fn main() {
///读取文件内容
fn read_file(path: &str) -> IResult<String> {
    let val = std::fs::read_to_string(path)?;
    Ok(val)
}

/// 转换为utf8内容
fn to_utf8(v: &[u8]) -> IResult<&str> {
    let x = std::str::from_utf8(v)?;
    Ok(x)
}

/// 转化为u32数字
fn to_u32(v: &str) -> IResult<u32> {
    let i = v.parse::<u32>()?;
    Ok(i)
}
}

std::result::Result<I, CustomError> 替换为:IResult<I>类型

当然,会有人提问,如果是多参数类型怎么处理呢,同样,我们只需将OK类型变成 tuple (I,O)类型的多参数数据即可,大概这样:


#![allow(unused)]
fn main() {
pub type IResult<I, O> = std::result::Result<(I, O), CustomError>;
}

使用也及其简单,只需要返回:I,O的具体类型,举个示例:


#![allow(unused)]
fn main() {
fn foo() -> IResult<String, u32> {
    Ok((String::from("bar"), 32))
}
}

使用重命名类型的Result,使得我们错误类型统一,方便处理。在实际项目中,可以大量看到这种例子的存在。

8. Option转换

我们知道,在Rust中,需要使用到unwrap()的方法的对象有Result,Option对象。我们看下Option的大致结构:


#![allow(unused)]
fn main() {
pub enum Option<T> {
    /// No value
    #[stable(feature = "rust1", since = "1.0.0")]
    None,
    /// Some value `T`
    #[stable(feature = "rust1", since = "1.0.0")]
    Some(#[stable(feature = "rust1", since = "1.0.0")] T),
}
}

Option本身是一个enum对象,如果该函数(方法)调用结果值没有值,返回None,反之有值返回Some(T)

如果我们想获取Some(T)中的T,最直接的方式是:unwrap()。我们前面说过,使用unwrap()的方式太过于暴力,如果出错,程序直接panic,这是我们最不愿意看到的结果。

Ok,那么我们试想下, 利用Option能使用?语法糖吗?如果能用?转换的话,是不是代码结构就更简单了呢?我们尝试下,代码如下:


#[derive(Debug)]
enum Error {
    OptionError(String),
}

impl std::error::Error for Error {}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self {
            Error::OptionError(ref e) => e.fmt(f),
        }
    }
}

pub type Result<I> = std::result::Result<I, Error>;


fn main() -> Result<()> {
    let bar = foo(60)?;
    assert_eq!("bar", bar);
    Ok(())
}

fn foo(index: i32) -> Option<String> {
    if index > 60 {
        return Some("bar".to_string());
    }
    None
}

执行结果报错:

error[E0277]: `?` couldn't convert the error to `Error`
  --> src/main.rs:22:22
   |
22 |     let bar = foo(60)?;
   |                      ^ the trait `std::convert::From<std::option::NoneError>` is not implemented for `Error`
   |
   = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
   = note: required by `std::convert::From::from`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `hyper-define`.

提示告诉我们没有转换std::convert::From<std::option::NoneError>,但是NoneError本身是unstable,这样我们没法通过From转换为自定义Error

本身,在Rust的设计中,关于OptionResult就是一对孪生兄弟一样的存在,Option的存在可以忽略异常的细节,直接关注目标主体。当然,Option也可以通过内置的组合器ok_or()方法将其变成Result。我们大致看下实现细节:


#![allow(unused)]
fn main() {
impl<T> Option<T> {
    pub fn ok_or<E>(self, err: E) -> Result<T, E> {
        match self {
            Some(v) => Ok(v),
            None => Err(err),
        }
    }
}    
}

这里通过ok_or()方法通过接收一个自定义Error类型,将一个Option->Result。好的,变成Result的类型,我们就是我们熟悉的领域了,这样处理起来就很灵活。

关于Option的其他处理方式,不在此展开解决,详细的可看下面链接:

延伸链接:https://stackoverflow.com/questions/59568278/why-does-the-operator-report-the-error-the-trait-bound-noneerror-error-is-no

9. 避免unwrap()

有人肯定会有疑问,如果需要判断的逻辑,又不用?这种操作,怎么取出OptionResult的数据呢,当然点子总比办法多,我们来看下Option如何做的:

fn main() {
    if let Some(v) = opt_val(60) {
        println!("{}", v);
    }
}

fn opt_val(num: i32) -> Option<String> {
    if num >= 60 {
        return Some("foo bar".to_string());
    }
    None
}

是的,我们使用if let Some(v)的方式取出值,当前else的逻辑就可能需要自己处理了。当然,Option可以这样做,Result也一定可以:

fn main() {
    if let Ok(v) = read_file("./dat") {
        println!("{}", v);
    }
}

fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

只不过,在处理Result的判断时,使用的是if let Ok(v),这个和Optionif let Some(v)有所不同。

到这里,unwrap()的代码片在项目中应该可以规避了。补充下,这里强调了几次规避,就如前所言:团队风格统一,方便管理代码,消除潜在危机

10. 自定义Error同级转换

我们在项目中,一个函数(方法)内部会有多次Result的结果判断:?,假设我们自定义的全局Error名称为:GlobalError

这时候,如果全局有一个Error可能就会出现如下错误:


#![allow(unused)]
fn main() {
std::convert::From<error::GlobalError<A>>` is not implemented for `error::GlobalError<B>
}

意思是:我们自定义的GlobalError没有通过From<GlobalError>转换我们自己自定义的GlobalError,那这样,就等于自己转换自己。注意:

  • 第一:这是我们不期望这样做的。
  • 第二:遇到这种自己转换自己的T类型很多,我们不可能把出现的T类型通通实现一遍。 这时候,我们考虑自定义另一个Error了,假设我们视为:InnnerError,我们全局的Error取名为:GlobalError,我们在遇到上面错误时,返回Result<T,InnerError>,这样我们遇到Result<T,GlobalError>时,只需要通过From<T>转换即可,代码示例如下:

#![allow(unused)]
fn main() {
impl From<InnerError> for GlobalError {
    fn from(s: InnerError) -> Self {
        Error::new(ErrorKind::InnerError(e))
    }
}
}

上面说的这种情况,可能会在项目中出现多个自定义Error,出现这种情况时,存在多个不同Error的std::result::Result<T,Err>的返回。这里的Err就可以根据我们业务现状分别反回不同类型了。最终,只要实现了From<T>trait可转化为最终期望结果。

11. Error常见开源库

好了,介绍到这里,我们应该有了非常清晰的认知:关于如何处理Rust的错误处理问题了。但是想想上面的这些逻辑多数是模板代码,我们在实际中,大可不必这样。说到这里,开源社区也有了很多对错误处理库的支持,下面列举了一些:

12. 参考链接

13 错误处理实战

这个例子介绍了如何在https://github.com/Geal/nom中处理错误,这里就不展开介绍了,有兴趣的可自行阅读代码。

详细见链接:https://github.com/baoyachi/rust-error-handle/blob/master/src/demo_nom_error_handle.rs

14. 总结

好了,经过上面的长篇大论,不知道大家是否明白如何自定义处理Error呢了。大家现在带着之前的已有的问题或困惑,赶紧实战下Rust的错误处理吧,大家有疑问或者问题都可以留言我,希望这篇文章对你有帮助。

文中代码详见:https://github.com/baoyachi/rust-handle-error/tree/master/src

原文地址:https://github.com/baoyachi/rust-error-handle

Rust crates私有化部署指南(private alternative registry)

Rust中,一般使用外部依赖的库部署在https://crates.io/。我们直接在Cargo.tomldependencies配置即可,列如下配置:

...
[dependencies]
rand = "0.7.3"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
lazy_static = "1.4.0"
...

如果细心的同学会发现,如果rand库更新到rand = "0.7.4"时,我们执行cargo updateCargo.lock会自动更新rand到最新的0.7.4版本。这主要是Rust采用了语义化版本

但是这样的依赖对于Rust提供的crates.io是没问题的,面对私有化的项目,即不能共享到公开的crates上,又想做到私有依赖,问题就暴露了。

2.cargo私有化git依赖

一般来说,企业的私有代码,都依赖于私有git仓库。翻过cargo的文档时,这里就提到了一个关于git私有化依赖的问题,详见链接:

这里的git私有化不外乎这几种依赖:branch依赖,tag依赖,commit-id依赖,列如下配置:

[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand", branch = "master" }
rand = { git = "https://github.com/rust-lang-nursery/rand", tag = "0.2.2" }
rand = { git = "https://github.com/rust-lang-nursery/rand", rev = "e2112c4" }

看到这里,我们就想到了才有私有化git方式依赖,我们也这样做了。接下来问题便发生了。

  • 2.1 在项目中,我们定义了一些公共依赖的结构:hula_common私有仓库。
  • 2.2 A项目(lib)使用了hula_common的仓库,依赖tag="0.1.5"如下
hula_common = { package = "hula_common", git = "ssh://git@git.baoyachi.com/hula/hula_common.git" , tag = "0.1.5" }
  • 2.3. B项目(lib)也使用了hula_common的仓库,依赖tag="0.1.4"如下
hula_common  = { package = "hula_common", git   = "ssh://git@git.baoyachi.com/hula/hula_common.git" , tag = "0.1.4" }
  • 2.4 Alib和Blib都同时依赖了hula_common,两者区别就是引用的git的tag版本不同。

  • 2.5 在主程序的project名称是fuzz中,fuzz同时依赖了A,B,hula_common等私有lib,大致如下:

...
[dependencies]
serde_derive = "1.0.111"
serde = "1.0.111"
serde_json = "1.0.53"
...

hula_common = { package = "hula_common", git = "ssh://git@git.baoyachi.com/hula/hula_common.git" , tag = "0.1.5" }
B = { package = "B", git = "ssh://git@git.baoyachi.com/hula/B.git" , tag = "0.3.2" }
A = { package = "A", git = "ssh://git@git.baoyachi.com/hula/A.git" , tag = "0.5.7" }
...
  • 2.6 当我们编译fuzz时,居然编译报错?
error[E0308]: mismatched types
   --> src/common/query.rs:196:23
    |
196 |         let items = self.query(pos, key)?;
    |                                ^^^^^ expected struct `hula_common::query::Container`, found struct `hula_common::query::Container`
    |
    = note: expected reference `&hula_common::query::Container`
               found reference `&hula_common::query::Container`
    = note: perhaps two different versions of crate `hula_common` are being used?

大致意思是:hula_common使用了不同版本(different versions),导致编译报错。这个很让人诡异: 对于上面提到的randcrates.io的依赖,如果版本更新,会自动更新,反而因为gittag差异,Rust认为导入的crate不一致,这个问题纠结了我很久。

一方面,想要让所有库依赖同一个tag,这个着实让人头疼。hula_common的定义由0.1.4->0.1.5,可能只是针对B项目增加的局部字段,而不会影响A项目的编译。按理说,若Rust遵循https://semver.org语义化版本的依赖,应该是可以正常编译过的,但现实是failed。

如果熟悉go mod的依赖,就会发现,上面这种依赖方式,在go mod编译时,是完全ok的。同样的问题,对于Rust来说,为何编译失败?因此,带着问题,寻求官方的回复,有人便给了反馈,原话如下:

Cargo has no way of knowing that two different tags point to compatible projects. If you want a shared dependency on a git project it has to be the same commit to be compatible. Cargo will happily build both tags into the same project, but it will not treat types from one as the same as types from the other.

...
git tags are arbitrary strings, we have no way to know if "master" and "dev" are sember compatible. crates.io versions are semver versions, so we can determine that "^0.1.4" can resolve to 0.1.5. Note that if you have "=0.1.4" as a requirement on crates.io you would also get an error.

Your company may want to create a private alternative registry.

大致意思是:Cargo无法知道两个不同的tag指向兼容的项目。如果要共享对git项目的依赖,则必须具有相同的commit才能兼容。本身git的tag可以是任意字符串,Cargo无法知道masterdev是否与semver versions的版本兼容。

如果真的要使用semver versionsCargo认识,需要创建类似crates.io的私有private alternative registry

3 alternative registry

Rust 1.34.0的版本中,支持了Alternative cargo registries。什么意思?通常我们不仅仅依赖crates.io上的代码,在企业或其他组织中,存在大量私有仓库代码(gitlab),使得我们在对于Cargo依赖时,既需要依赖crates.io,又要有其他的registry依赖,1.34.0的版本开始支持了该特性。大致配置如下:

  • 3.1 创建在git上的crate index的存储仓库 假设我们在gitlab上创建了一个关于私有的crates的存储仓库:https://git.baoyachi.com/hula/crates-index.git

  • 3.2 找到.cargo的根目录,创建config.toml文件(./cargo/config.toml),在config.toml配置自己的registry,假设我们命名:git-baoyachi, 并做如下配置:

[registries]
git-baoyachi = { index = "https://git.baoyachi.com/hula/crates-index" }

或是这样:

[registries.git-baoyachi]
index = "https://git.baoyachi.com/hula/crates-index"

上面两者是等价的,二选一即可。

注意:假设私有仓库是gitlab,这里的index的链接即为gitlab的链接,即为:https://git.baoyachi.com/hula/crates-index.git。这里我们使用的是httphttps的方式,如果是ssh的话,列配置如下:


#![allow(unused)]
fn main() {
[registries.git-baoyachi]
index = "ssh://git@git.baoyachi.com/hula/crates-index.git"
}

或是这样:


#![allow(unused)]
fn main() {
[registries]
git-baoyachi = {index = "ssh://git@git.baoyachi.com/hula/crates-index.git"}
}

请注意:从gitlab的克隆按钮复制/粘贴时,SSH网址有两个主要区别:必须在前面添加ssh://,并将gi.xxx.com之后的:更改为/

  • 3.3 配置crates备用注册表的服务 如果读了文档的,细心的同学会发现,文档中只是介绍了怎么配置,因为并没有告诉怎么搭建私有的alternative registry。好在开源社区提供了私有化的crates的注册服务:https://github.com/Hirevo/alexandrie

我们跟着文档手册手动搭一个试试:https://hirevo.github.io/alexandrie/installation-script.html

  • 3.3.1 创建安装配置脚本:alexandrie.sh
#!/bin/bash

# function to run when an error is encountered
function setup_error {
    echo "-------- An error occurred during configuration --------"
    exit 1
}

# exit on error
trap 'setup_error' ERR

# directory to clone Alexandrie into:
ALEXANDRIE_DIR="$2";

# URL to the crate index repository.
CRATE_INDEX_GIT_URL="$1";


while ! git ls-remote -h $CRATE_INDEX_GIT_URL; do
    read -p 'CRATE_INDEX_GIT_URL: ' CRATE_INDEX_GIT_URL;
done

if ! cargo -V; then
    echo;
    echo "In order to build an instance of Alexandrie, you need to have Rust installed on your system";
    echo "You can learn how to install Rust on your system on the official Rust website:";
    echo "https://www.rust-lang.org/tools/install";
    echo;
    ! :;    # trigger error trap
fi

if [ -d "$ALEXANDRIE_DIR" ]; then
    echo
    echo "'$ALEXANDRIE_DIR' (ALEXANDRIE_DIR) is an existing directory, pulling latest changes ...";
    cd "$ALEXANDRIE_DIR";
    git pull;
    echo "Changes have been pulled successfully !";
    echo;
else
    echo;
    echo "Cloning Alexandrie in '$ALEXANDRIE_DIR' ...";
    git clone https://github.com/Hirevo/alexandrie.git "$ALEXANDRIE_DIR";
    cd "$ALEXANDRIE_DIR";
    echo "Successfully cloned Alexandrie !";
    echo;
fi

echo "Building Alexandrie (using the default features)...";
echo "(keep in mind that the default features may not fit your use-case, be sure to review them before deplying it to production)";
cargo build -p alexandrie;
echo "Alexandrie has been built successfully !";

# create the directory serving as the storage of crate archives.
mkdir -p crate-storage;

# setup the crate index.
if [ -d crate-index ]; then
    echo;
    echo "'${ALEXANDRIE_DIR}/crate-index' is an existing directory, pulling latest changes ...";
    cd crate-index;
    git pull;
    echo "Changes have been pulled successfully !";
    echo;
else
    echo;
    echo "Cloning crate index in '${ALEXANDRIE_DIR}/crate-index' ...";
    git clone "$CRATE_INDEX_GIT_URL" crate-index;
    cd crate-index;
    echo "Successfully cloned the crate index !";
    echo;
fi

# configure the crate index
if [ ! -f config.json ]; then
    echo "The crate index does not have a 'config.json' file.";
    echo "Creating an initial one (please also review it before deploying the registry in production) ..."
    cat > config.json << EOF;
{
    "dl": "http://$(hostname):3000/api/v1/crates/{crate}/{version}/download",
    "api": "http://$(hostname):3000",
    "allowed-registries": ["https://github.com/rust-lang/crates.io-index"]
}
EOF
    git add config.json;
    git commit -m 'Added `config.json`';
    git push -u origin master;
    echo "Initial 'config.json' file has been created and pushed to the crate index !";
    echo;
fi

echo "Alexandrie should be good to go for an initial run.";
echo "You can start the Alexandrie instance by:";
echo "  - navigating to '${ALEXANDRIE_DIR}'";
echo "  - tweaking the 'alexandrie.toml' file";
echo "  - run `./target/debug/alexandrie`";
echo;

注意:该脚本需要两个参数:

  • ALEXANDRIE_DIR: 本地的路径,列如:/home/baoyachi/alexandrie

  • CRATE_INDEX_GIT_URL:就是我们在3.1创建的crats index的git地址,如:ssh://git@git.baoyachi.com/hula/crates-index.git

  • 3.3.2 执行脚本

sh alexandrie.sh ssh://git@git.baoyachi.com/hula/crates-index.git /home/baoyachi/alexandrie

解释下该脚本做的工作:clone CRATE_INDEX_GIT_URL仓库,创建config.json,配置如下:

{
    "dl": "http://{{host:port}}/api/v1/crates/{crate}/{version}/download",
    "api": "http://{{host:port}}",
    "allowed-registries": ["https://github.com/rust-lang/crates.io-index"]
}

这里的host,port最终换成线上部署的服务。

  • 3.3.3 启动服务
cd /home/baoyachi/alexandrie
./target/debug/alexandrie alexandrie.toml

服务启动完成,大致输出如下:

➜  alexandrie git:(master) ./target/debug/alexandrie alexandrie.toml
Jul 26 22:23:36.382 INFO running database migrations, version: 0.1.0
Jul 26 22:23:36.383 INFO setting up request logger middleware, version: 0.1.0
Jul 26 22:23:36.383 INFO setting up cookie middleware, version: 0.1.0
Jul 26 22:23:36.384 INFO setting up authentication middleware, version: 0.1.0
Jul 26 22:23:36.384 INFO mounting '/', version: 0.1.0
Jul 26 22:23:36.384 INFO mounting '/me', version: 0.1.0
Jul 26 22:23:36.384 INFO mounting '/search', version: 0.1.0
Jul 26 22:23:36.385 INFO mounting '/most-downloaded', version: 0.1.0
Jul 26 22:23:36.385 INFO mounting '/last-updated', version: 0.1.0
Jul 26 22:23:36.385 INFO mounting '/crates/:crate', version: 0.1.0
Jul 26 22:23:36.386 INFO mounting '/account/login', version: 0.1.0
Jul 26 22:23:36.386 INFO mounting '/account/logout', version: 0.1.0
Jul 26 22:23:36.386 INFO mounting '/account/register', version: 0.1.0
...
...
Jul 26 22:23:36.391 INFO mounting '/api/v1/crates/:name', version: 0.1.0
Jul 26 22:23:36.391 INFO mounting '/api/v1/crates/:name/owners', version: 0.1.0
Jul 26 22:23:36.392 INFO mounting '/api/v1/crates/:name/:version/yank', version: 0.1.0
Jul 26 22:23:36.392 INFO mounting '/api/v1/crates/:name/:version/unyank', version: 0.1.0
Jul 26 22:23:36.392 INFO mounting '/api/v1/crates/:name/:version/download', version: 0.1.0
Jul 26 22:23:36.392 INFO listening on 127.0.0.1:3000, version: 0.1.0
Jul 26 22:23:36.392 INFO Server listening on http://127.0.0.1:3000, version: 0.1.0
  • 3.4 执行cargo login 在执行cargo login --registry=my-registry,这里的my-registry即为3.2配置的index名称,假设为:git-baoyachi,则执行
➜ cargo login --registry=git-baoyachi
please visit http://localhost:3000/me and paste the API Token below
2506b7bd94dfc708b755d5399
       Login token for `git-baoyachi` saved

提示我们需要输入token,我们在3.3.3启动的本地服务,访问http://localhost:3000/me,系统提示我们注册,生成token,大致如下: token

  • 3.5 创建,发布私有crates库 假设我们创建了库,cargo new hula-common --lib,我们将该库发布到指定的私有git仓库中,如3.1所述,执行如下命令,这里的my-registry即为3.2配置的index名称,假设为:git-baoyachi,则执行
cargo publish --registry=git-baoyachi

如果login成功的话,执行结果大致如下:

➜  cargo publish --registry=git-baoyachi 
    Updating `ssh://git@git.baoyachi.com/hula/crates-index.git` index
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
   Packaging hula-common v1.0.2 (/home/baoyachi/git_project/crates-index/hula-common)
   Verifying hula-common v1.0.2 (/home/baoyachi/git_project/crates-index/hula-common)
   Compiling hula-common v1.0.2 (/home/baoyachi/git_project/crates-index/hula-common/target/package/h2-1.0.2)
    Finished dev [unoptimized + debuginfo] target(s) in 1.18s
   Uploading hula-common v1.0.2 (/home/baoyachi/git_project/crates-index/hula-common)
➜     

注意,alexandrie会检查Cargo.toml的版本version = "1.0.3",如果同一版本publish多次,会报如下错误:

error: failed to get a 200 OK response, got 500
headers:
	HTTP/1.1 100 Continue

	HTTP/1.1 500 Internal Server Error

	content-length: 0

	date: Sun, 26 Jul 2020 10:42:46 GMT

body:

我们需要检查当前库的版本号是否重复即可。如果按上步骤执行ok的话,alexandrie会自动push我们仓库的私有crates的git仓库,更新当前publish库的信息。

4. 使用私有crates依赖

我们在第一章节也详细描述了,如果使用git的branchtag依赖时,会出现当前perhaps two different versions of crate的问题,我们花了大量篇幅讲解如果使用创建和发布私库,就是为了解决git依赖的问题。那么,答案来了,我们只需要修改Cargo.tomldependencies依赖即可,我们还是以第一章列子举例配置:

...
[dependencies]
serde_derive = "1.0.111"
serde = "1.0.111"
serde_json = "1.0.53"
...

hula_common = { version="0.1.4", registry = "git-baoyachi" }
B = { package = "B", version="0.3.2", registry = "git-baoyachi" }
A = { package = "A", version="0.5.7", registry = "git-baoyachi" }
...

我们只需要在依赖后面,指定自定义的registry配置即可。这样,即使hula_common升级到version="0.1.5",我们执行cargo update也可以让正常编译。

5.总结

我们想建立私有的crates的配置,大致需要如下几步:

  • 创建存储crates-index的私有仓库
  • 部署 alexandrie的私有服务,绑定域名
  • 建立认证,获取cargo logintoken
  • 对应lib执行cargo publish
  • 使用依赖,配置指定registry

写在最后

关于这部分的问题,Rust的相关资料在中文社区,网上的查到还是较少,一方面是语言普及度的原因,另一方面是社区分享的少。但这并不妨碍我们解决问题。对于先吃螃蟹的,总会磕磕绊绊,在这解决问题的过程中,对于问题的刨根,和开源社区交流,沟通以及协作上的思维方式还是有不少收获。

参考链接

Rust代码优化-闭包惰性求值

code_practice

背景

写代码时,经常会遇到从OptionResult中取值得问题,为了避免使用unwrap(),使用了if let Some(xxx)if let Ok(xxx) 的代码表达。之前的文章也介绍了这种写法,惯用法也自以为习以为常。

  • 1.最近一次代码在发PR时,下面这段代码作者建议,又有了写收获:

#![allow(unused)]
fn main() {
struct Message(usize);

fn get_msg(msg: &Option<Message>) -> String {
    let mut rep_msg = "Ok".to_string();
    if let Some(msg) = msg {
        rep_msg = format!("{}", msg.0)
    }
    rep_msg
}
}
  • 2.为了避免resp_msg使用mut,建议改使用下面这种方式:

#![allow(unused)]
fn main() {
fn get_msg(msg: &Option<Message>) -> String {
    let rep_msg = if let Some(msg) = msg {
        format!("{}", msg.0)
    } else {
        "Ok".to_string()
    };
    rep_msg
}
}

通过赋值方式传递rep_msg,避免了rep_msg的可变性。

  • 3.本身上面代码看起来还是有点绕口,为了表达直观,通过代码块{},可表达为下面方式:

#![allow(unused)]
fn main() {
fn get_msg(msg: &Option<Message>) -> String {
    let rep_msg = {
        if let Some(msg) = msg {
            format!("{}", msg.0)
        } else {
            "Ok".to_string()
        }
    };
    rep_msg
}
}
  • 4.但是上面代码看起来还是有点啰嗦,我们继续优化,可以采用Option的组合子方法,链式表达,修改如下

#![allow(unused)]
fn main() {
fn get_msg(msg: &Option<Message>) -> String {
    msg.as_ref().map(|msg| format!("{}", msg.0)).unwrap_or("Ok".to_string())
}
}

是不是使用combinator的模式,代码更简单、直观了。但是,这里还是有点缺陷,我们继续往下看:

  • 5.如果使用cargo clippy的使用,clippy会经常提示我们修改这段逻辑:

#![allow(unused)]
fn main() {
msg.unwrap_or("Ok".to_string())
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `unwrap_or_else(|| "OK".to_string())`
                                          |
}

为什么呢?对于unwrap_or,即使不使用其值,参数"Ok".to_string()也将进行求值。对于unwrap_or_else,参数接收一个闭包, 当触发unwrap_or_else调用时,通过传递函数(闭包)进行惰性求值(lazy evaluation)计算,提升程序性能,那我们改造下


#![allow(unused)]
fn main() {
fn get_msg(msg: &Option<Message>) -> String {
    msg.as_ref().map(|msg| format!("{}", msg.0)).unwrap_or_else(||"Ok".to_string())
}
}

从第一个函数看到现在,有么有收获呢?欢迎大家在评论区留言,一起交流。

最后

推荐大家在Rust项目中,将 clippy 这个插件使用起来,优化程序的结构。让优秀成为一种习惯。

参考链接

shadow_rs

构建你的Rust编译版本信息

有时,我们构建的Rust二进制包需要交付给它人使用。难免存在需要追溯当前二进制包的版本信息的问题。包括构建的包系统环境(OS),Rust版本代码分支commit_id等常用信息。 有了这些信息,不管是定位问题还是复现bug,都清晰很多。

思考

通过获取环境变量或外部参数,将信息编译统一打包到编译文件中。好在Rust提供了build script的功能,详见链接:build-scripts.html. 因此我们可以通过这种方式将编译信息生成出对应的rust代码,通过Rust提供的include!宏来讲生成代码和目标代码打包。我们就可以在项目中使用我们生成的静态信息了。

使用 shadow-rs

https://github.com/baoyachi/shadow-rs

获取环境变量,获取外部参数,判断当前构建环境,直到生成代码。这一系列过程,本身不复杂,需要判断条件,索性就写了一个构建脚本的工具,使用如下

step1

在当前项目目录下的Cargo.toml文件,配置如下信息

[package]
build = "build.rs"

[build-dependencies]
shadow-rs = "0.3"

step 2

在当前项目的build.rs文件下,配置如下:

fn main() -> shadow_rs::SdResult<()> {
    let src_path = std::env::var("CARGO_MANIFEST_DIR")?;
    let out_path = std::env::var("OUT_DIR")?;
    shadow_rs::Shadow::build(src_path, out_path)?;
    Ok(())
}

step 3

在当前项目下找到bin文件,通常是main.rs文件,你可以在Cargo.toml[bin]配置找到当前主文件,然后添加如下信息


#![allow(unused)]
fn main() {
pub mod shadow{
    include!(concat!(env!("OUT_DIR"), "/shadow.rs"));
}
}

step 4

然后你就可以使用shadow的常量信息了,大概如下

fn main() {
    println!("{}",shadow::BRANCH); //master
    println!("{}",shadow::COMMIT_HASH);//8405e28e64080a09525a6cf1b07c22fcaf71a5c5
    println!("{}",shadow::SHORT_COMMIT);//8405e28e
    println!("{}",shadow::COMMIT_DATE);//2020-08-16T06:22:24+00:00
    println!("{}",shadow::COMMIT_AUTHOR);//baoyachi
    println!("{}",shadow::COMMIT_EMAIL);//xxx@gmail.com

    println!("{}",shadow::BUILD_OS);//macos-x86_64
    println!("{}",shadow::RUST_VERSION);//rustc 1.45.0 (5c1f21c3b 2020-07-13)
    println!("{}",shadow::RUST_CHANNEL);//stable-x86_64-apple-darwin (default)
    println!("{}",shadow::CARGO_VERSION);//cargo 1.45.0 (744bd1fbb 2020-06-15)
    println!("{}",shadow::PKG_VERSION);//0.3.13
    println!("{}",shadow::CARGO_LOCK);

    println!("{}",shadow::PROJECT_NAME);//shadow-rs
    println!("{}",shadow::BUILD_TIME);//2020-08-16 14:50:25
    println!("{}",shadow::BUILD_RUST_CHANNEL);//debug

    ...
}

通常,你会使用clapcrates库.

详细使用,请参考链接例子:https://github.com/baoyachi/shadow-rs/tree/master/example_shadow

原文地址

Rust的Trait clone

背景

示例

use std::fmt::Debug;

pub trait DynClone {
    fn clone_box(&self) -> Box<dyn AbsClone>;
}

pub trait AbsClone: Send + Sync + DynClone + Debug {}

impl Clone for Box<dyn AbsClone> {
    fn clone(&self) -> Self {
        self.clone_box()
    }
}

#[derive(Clone)]
struct Foo {
    bar: Box<dyn AbsClone>,
}

impl<T> DynClone for T
where
    T: Clone + AbsClone + 'static,
{
    fn clone_box(&self) -> Box<dyn AbsClone> {
        Box::new(self.clone())
    }
}

#[derive(Clone, Debug)]
struct Bar {
    name: &'static str,
}

impl AbsClone for Bar {}

fn main() {
    let bar = Box::new(Bar { name: "hello bar" });
    let foo = Foo { bar };
    // expect print: Bar { name: "hello bar" }
    println!("{:?}", foo.bar);
}

参考

  • https://github.com/nushell/nushell/blob/1784b4bf50/crates/nu-protocol/src/engine/command.rs#L103-L120
  • https://github.com/dtolnay/dyn-clone/blob/1.0.9/src/lib.rs

延伸

  • https://crates.io/crates/typetag

golang

golang 杂谈

lint工具

格式化代码

  • gofmt -w ./

引用链接

编码规范参考

参考uber golang编码规范

  • https://github.com/uber-go/guide
  • https://github.com/xxjwxc/uber_go_guide_cn

常用工具

Github下载加速

详情链接:[https://gh.com/](https://g.ioiox.com/)

mdbook theme