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(())
}
相关推荐
世俗ˊ22 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92122 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_27 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人36 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css
清汤饺子1 小时前
实践指南之网页转PDF
前端·javascript·react.js
蒟蒻的贤1 小时前
Web APIs 第二天
开发语言·前端·javascript