channel 转移数据

文章完全参考 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(())
}
相关推荐
熊的猫44 分钟前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596933 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai3 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书