介绍
- github: https://github.com/baoyachi
- zhihu: https://www.zhihu.com/people/baoyachi
follow
blog
flow about tech blog.
Rust
video
Rust
- Rust Linux 的内存分析器:https://github.com/koute/
细说Rust错误处理
原文地址: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代码中,应该看过很多这种代码,甚至此时我们正在使用它。它主要用于Option
或Result
的打开其包装的结果。常常我们在代码中,使用简单,或快速处理,使用了 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
的枚举类型为Ok
和Err
,使得我们每次在返回Result
的结果时,要么是Ok
,要么是Err
。它不需要return
结果同时给两个值赋值,这样的情况只会存在一种可能性: Ok or Err 。- golang的函数调用需要对
if err!=nil
做结果判断,因为这段代码 判断是手动逻辑,往往我们可能因为疏忽,导致这段逻辑缺失,缺少校验。当然,我们在编写代码期间可以通过某些工具lint
扫描出这种潜在bug。 Rust
的match
判断是自动打开,当然你也可以选择忽略其中某一个枚举值,我们不在此说明。
可能有人发现,如果我有多个函数,需要多个函数的执行结果,这样需要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,需要返回子Error:Some(err)
,需要覆盖该方法。
- 如果当前
-
type_id()
该方法被隐藏。 -
backtrace()
返回发生此错误的堆栈追溯,因为标记unstable
,在Rust
的stable
版本不被使用。 -
自定义的
Error
需要impl std::fmt::Debug的trait,当然我们只需要在默认对象上添加注解:#[derive(Debug)]
即可。
总结一下,自定义一个error
需要实现如下几步:
- 手动实现impl
std::fmt::Display
的trait,并实现fmt(...)
方法。 - 手动实现impl
std::fmt::Debug
的trait
,一般直接添加注解即可:#[derive(Debug)]
- 手动实现impl
std::error::Error
的trait
,并根据自身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
为我们实现的自定义ErrorCustomError
有三个子类型ErrorCustomError
分别实现了三个子类型ErrorFrom
的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
结果,并且Result
的Err
类型是我们自定义错误
,导致我们写程序时会显得非常啰嗦、冗余
#![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
的设计中,关于Option
和Result
就是一对孪生兄弟一样的存在,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
的其他处理方式,不在此展开解决,详细的可看下面链接:
9. 避免unwrap()
有人肯定会有疑问,如果需要判断的逻辑,又不用?
这种操作,怎么取出Option
或Result
的数据呢,当然点子总比办法多,我们来看下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)
,这个和Option
的if 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<GlobalErrorGlobalError
,那这样,就等于自己转换自己。注意:
- 第一:这是我们不期望这样做的。
- 第二:遇到这种自己转换自己的
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
的错误处理问题了。但是想想上面的这些逻辑多数是模板代码,我们在实际中,大可不必这样。说到这里,开源社区也有了很多对错误处理库的支持,下面列举了一些:
- https://github.com/rust-lang-nursery/failure
- https://github.com/rust-lang-nursery/error-chain
- https://github.com/dtolnay/anyhow
- https://github.com/dtolnay/thiserror
- https://github.com/tailhook/quick-error
12. 参考链接
- https://blog.burntsushi.net/rust-error-handling/
- https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/question-mark-in-main-and-tests.html
- https://doc.rust-lang.org/rust-by-example/error/result.html
- https://doc.rust-lang.org/rust-by-example/error.html
- https://github.com/rust-lang/rust/issues/43301
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.toml
的dependencies
配置即可,列如下配置:
...
[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 update
,Cargo.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
A
lib和B
lib都同时依赖了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),导致编译报错。这个很让人诡异:
对于上面提到的rand
在crates.io
的依赖,如果版本更新,会自动更新,反而因为git
的tag
差异,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
无法知道master
和dev
是否与semver versions
的版本兼容。
如果真的要使用semver versions
让Cargo
认识,需要创建类似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
。这里我们使用的是http
或https
的方式,如果是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,大致如下:
- 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的branch
或tag
依赖时,会出现当前perhaps two different versions of crate
的问题,我们花了大量篇幅讲解如果使用创建和发布私库,就是为了解决git依赖的问题。那么,答案来了,我们只需要修改Cargo.toml
的dependencies
依赖即可,我们还是以第一章列子举例配置:
...
[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 login
的token
- 对应
lib
执行cargo publish
- 使用依赖,配置指定
registry
写在最后
关于这部分的问题,Rust的相关资料在中文社区,网上的查到还是较少,一方面是语言普及度的原因,另一方面是社区分享的少。但这并不妨碍我们解决问题。对于先吃螃蟹的,总会磕磕绊绊,在这解决问题的过程中,对于问题的刨根,和开源社区交流,沟通以及协作上的思维方式还是有不少收获。
参考链接
- https://github.com/rust-lang/cargo/issues/8536
- https://doc.rust-lang.org/cargo/reference/registries.html#using-an-alternate-registry
- https://github.com/Hirevo/alexandrie/issues/75
- https://blog.rust-lang.org/2019/04/11/Rust-1.34.0.html
- https://github.com/rust-lang/rfcs/blob/master/text/2141-alternative-registries.md
Rust代码优化-闭包惰性求值
背景
写代码时,经常会遇到从Option
或Result
中取值得问题,为了避免使用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 这个插件使用起来,优化程序的结构。让优秀成为一种习惯。
参考链接
- https://github.com/rust-lang/rust/pull/55014
- https://stackoverflow.com/questions/56726571/why-choosing-unwrap-or-else-over-unwrap-or
- https://stackoverflow.com/questions/45547293/why-should-i-prefer-optionok-or-else-instead-of-optionok-or
构建你的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 ... }
通常,你会使用clap
的crates
库.
详细使用,请参考链接例子: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/)