文章完全参考 Channels 的内容,并没有进行直接翻译,而是使用另一个 redis 库来验证学习文章的例子。在了解 channel 的同时也掌握一些 redis 的编程。首先,我们需要在本机安装一个 redis 实例。
redis 安装
Mac 可以通过 brew 直接安装 redis ,运行如下命令:
bash
brew install redis
不过,时间长了,我们也记不清楚本机上究竟安装了哪些服务,好在可以通过 brew services 来查看本机安装的服务。安装完成后,
启动 redis 服务 :
bash
brew services start redis
停止 redis 服务:
bash
brew services stop redis
被安装的redis的配置文件路径,这个路径比较关键,涉及到修改配置
bash
/usr/local/etc/redis.conf
还可以通过 redis-cli 建立连接,当然,也可以通过更加原始的 telnet 指令建立连接
bash
redis-cli ping
最后一步,从本机卸载 redis
bash
brew uninstall redis
使用 redis-rs 库
Hello Tokio 的示例代码中,引入了 mini_redis 三方依赖,但这个库本身是不完善的。在它的文档中也明确声明:不要将 mini_redis 引入到生产环境中,这个库只是为 tokio 做教学使用的。
文章决定采用 redis-rs 的库来进行替换,搜索 rust redis 关键字, redis 相关的三方库也挺多的,这个算是比较官方的,这都不重要,channel 才是本文的主角。
关键的一步,给当前示例引入 redis 依赖
rust
[dependencies]
redis = "0.23.3"
下面是使用 redis 的基本例子,首先和本地的 redis 建立连接,然后,调用 set 命令写一个 kv 的数据。最后可以在控制台使用 get 来验证写入是否成功了。
rust
extern crate redis;
use redis::Commands;
fn main() {
do_something();
}
fn do_something() -> redis::RedisResult<()> {
let client = redis::Client::open("redis://127.0.0.1:6379/")?;
let mut conn = client.get_connection()?;
let _: () = conn.set("shop:neo", 35)?;
Ok(())
}
问号 ? 的使用
非常有必要来聊一聊 ?的使用情况,如果不熟悉 rust 的话,初次接触问号可能会觉得特别不能理解,我就属于这样,特别想搞清楚。
? 的使用让人很迷惑,硬要去参考Go的话,类似 ... 的语法糖,作为一种简化逻辑的手段,也好像是三目运算符。了解问号,不外乎回答清楚 2 个问题:①它的作用是什么?②什么时候使用?
参考《The Rust Programming Language 》附录中的描述,?的表达式如下:
expr?
Error propagation
针对可能出错的操作,可以使用 ? 来做逻辑简化。Go 语言中也有很多可能会出错的系统调用,一般的处理模式都是提供两个返回值,其中一个是 error 类型,当error 不为 nil 时,表示方法执行成功了,然后去使用另一个返回结果。比如,随意选取 go http 包下的一个方法:
go
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error)
很多时候,考虑要不要处理 error 很头疼。不处理吧,担心它会失败,处理吧,感觉怎么也不应该失败。不过,如果某个函数调用触发了 error ,直接沿着栈的调用链向上抛是最简单的处理方式。当然,也有心累的时候,只要调用的函数中返回了 error,那就无脑处理 error 也没啥问题,每个可能出错的函数都判断一下 error 是否为空,为空的话,就终止当前执行直接向上层抛出 error。
现在已经明确了 ?处理的对象是可能会出错的函数,比如,例子中的 redis 调用 open 函数建立链接,网络总是有不可靠的时候,出错在所难免。
那么,?是如何处理错误的呢?因为单调所有简单,处理方式便是沿着调用栈向上传播错误。如果函数没有返回错误就继续执行,如果有错误,就自动终止当前流程,return 这个错误。
不过,这个自动向上传递的错误类型让我很迷,rust 中的 error 没有类似 go 语言接口 interface 的定义,我向上传递 error 的类型需要符合函数的声明定义。但上面例子中的 ?错误传递链路,是如何保证 error 能顺利地被传递呢?
这引出了 ?另一个关键性问题,它操作的对象类型是什么,这个对象类型需要能明确标志出成功和失败,成功的时候返回结果,失败的时候返回错误。另外,可能也需要有类似 Go interface 的多态性类型,兼容一下类型约束。
Result 类型
可以推断,?也不是想用就能用的,还是需要有类型限制,?返回的类型必须和调用函数返回的类型相一致。基于当前的例子,我们关注到函数的返回值类型是:redis::RedisResult<()>,整个方法对比着看一下,方法和返回值的声明:
Open RedisResult<Client>
get_connection RedisResult<Connection>
set RedisResult<RV>
RedisResult 看起来是类型的核心,尖括号中的类型各不相同,其实是 rust 泛型的声明。针对这样的类型声明,如果程序正常执行,完全可以理解函数成功时返回的结果,但如果 ? 发生了 error 呢,这个类型是如何操作的呢?
跳转到类型声明:RedisError类型,其实是 Result 类型,到此为止,我们找到了 rust 处理 error 的关键类型 Result。按照 Go 语言的说法,这种属于定义了一个新的类型。
rust
/// Library generic result type.
pub type RedisResult<T> = Result<T, RedisError>;
Result的枚举定义参考官方的文档地址:std::result - Rust (rust-lang.org)
rust
Enum Result<T,E> {
Ok(T),
Err(E),
}
类型声明包含两个枚举项 Ok 和 Err,如果枚举项为 Ok 则表示结果正常,枚举项的属性就是正常的返回值;如果枚举项是 Err 则表示发生了错误,表示发生异常时返回的错误信息
关于 Result 的枚举类型,算是比较奇怪的声明形式。拿结构体来类比,结构体需要包含一些列字段声明,而 Result 中的 Ok和 Err 就显得格格不入,这种Ok() 的写法看来像是方法的调用,或者是类型强制转换。说到底,我就是很纠结这种写法:
不过,结合我们上面的代码,最后函数的返回值:Ok(()),我们可能也会明白 Ok 缩代表的确切含义。返回值 Ok(()) 的类型就是 Result 的类型,它就是一个枚举中的一个值。
rust
Ok(T)
Contains the success value
Err(E)
Contains the error value
最后的结果,既然可以返回 Ok(()),当然,也可以返回Err()。但问题是,我怎么才能自定义个符合条件的 error 类型呢。最简单的当然是直接声明这样一个结构体了,很遗憾,这个结构体字段成员是私有的。
rust
pub struct RedisError { /* private fields */ }
好在,曲线救国,经过几次尝试,还是顺利的把错误给返回了。首先声明了一个其他类型的std::io::error,然后将这个 error 转换为 RedisError 类型。仔细看 RedisResult 的函数说明,其实也还有很多 error 转换的类型
rust
extern crate redis;
use redis::{Commands, RedisError};
use std::io::{Error, ErrorKind};
fn main() {
do_something();
}
fn do_something() -> redis::RedisResult<()> {
let client = redis::Client::open("redis://127.0.0.1:6379/")?;
let mut conn = client.get_connection()?;
let _: () = conn.set("shop:neo", 35)?;
// Ok(())
let custom_error = Error::new(ErrorKind::Other, "oh no!");
Err(RedisError::from(custom_error))
}
对于 Err,也不用这么纠结,去Result的官方文档,代码示例能给我们一些使用启发。对于 Err(E),Err() 用来表示错误触发了,E 表示错误触发后返回的值类型。它其实是两层含义了,第一层说明这个操作发生异常了;第二层表示异常后返回了类型为 E 的一个值。
Result 读取文件
我们使用 rust 读取文件的例子来使用正向看 Result 的使用,如果不使用 ?,我们该如何处理处理 Result 类型。首先是使用 ?的简单处理方式:
rust
use std::fs::File;
use std::io::prelude::*;
fn main() -> std::io::Result<()> {
let mut file = File::open("foo.txt")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
assert_eq!(contents, "Hello, world!");
Ok(())
}
给 main 方法指定了返回值,rust 真有你的!我很难理解: 调用 main 的函数使用这个返回值来干什么,以及我该如何使用这个返回值?这不是重点...
下面是不使用 ?的简单处理方式,针对每个 Result 都使用 match 表达式来处理所有可能的 Result。虽然过程显得有些冗余,但确实比较常规的处理方式。
open 函数运行失败返回的 Err 类型和 read_to_string 函数相同,都是属于 std::io::Error 类型。因为两个函数错误类型相同,所以程序可以正常执行。
rust
use std::fs::File;
use std::io::prelude::*;
fn main() -> std::io::Result<()> {
let mut file = match File::open("Cargo.lock") {
Err(e) => return Err(e),
Ok(f) => f,
};
let mut contents = String::new();
let size = match file.read_to_string(&mut contents) {
Err(e) => return Err(e),
Ok(s) => s,
};
print!("size:{},content:{}", size, contents);
Ok(())
}
例子中直接读取项目下的 Cargo.lock 完全是处于演示的目的,现在我们专门创建一个 .json 文件,文件内容符合下面结构体的声明,然后将文件中的内容反序列到结构体中。这算的上一个比较常规的操作,而且,这个过程会发生错误。
为什么结构体声明要带这些专有的注释呢,这写注释可是会被编译器识别并特殊处理的。Go里面也有一些注释语法,但没有像 rust 这样疯狂输出。
rust
#[derive(Debug, Serialize, Deserialize)]
struct Spot {
lat: f32,
lng: f32,
}
json 反序列的逻辑很简单,引入下面的依赖项目,我们将依赖 serde_json 来对结构体进行反序列化。
toml
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
下面是反序列的全部代码,程序正常输出了反序列的结构体。我表示特别遗憾:我以为函数 from_str 返回的 Err 会和 std::io::Error 类型不同,从而导致程序运行出错,但程序正常执行完成了
rust
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::prelude::*;
#[derive(Debug, Serialize, Deserialize)]
struct Spot {
lat: f32,
lng: f32,
}
fn main() -> std::io::Result<()> {
let mut file = match File::open("spot.json") {
Err(e) => return Err(e),
Ok(f) => f,
};
let mut contents = String::new();
let size = match file.read_to_string(&mut contents) {
Err(e) => return Err(e),
Ok(s) => s,
};
print!("size:{},content:{}", size, contents);
let point: Spot = serde_json::from_str(&contents)?;
print!("{:?}", point);
Ok(())
}