项目概况
我有一个名为Hashtrack的家庭项目。这是我为技术面试编写的一个小型站点,全栈应用程序。使用它非常简单:
- 用户已通过身份验证(假设他已经为自己创建了一个帐户)。
- 他介绍了他想观看的主题标签出现在Twitter上。
- 他等待找到的带有指定主题标签的tweet出现在屏幕上。
您可以在此处测试Hashtrack 。
在完成采访后,出于体育兴趣,我继续从事该项目,并且我发现这可能是一个不错的平台,可以在此测试我在命令行工具开发领域的知识和技能。我已经有一个服务器,所以我只需要选择一种语言,就可以在我的项目的API中实现一小组功能。
命令行工具功能
这里是主要功能的描述,尤其是-我想在命令行工具中实现的命令。
hashtrack login
-登录系统,即-创建会话令牌并将其保存在本地文件系统的配置文件中。hashtrack logout
— , — , .hashtrack track <hashtag> [...]
— .hashtrack untrack <hashtag> [...]
— .hashtrack tracks
— , .hashtrack list
— 50 .hashtrack watch
— .hashtrack status
— , .-
--endpoint
, . -
--config
, . -
endpoint
.
在开始使用该工具之前,需要考虑以下重要事项:
- 它应该使用使用GraphQL,HTTP和WebSocket的项目API。
- 它必须使用文件系统来存储配置文件。
- 它应该能够解析位置参数和命令行标志。
为什么我决定使用Go and Rust?
您可以使用多种语言编写命令行工具。
在这种情况下,我想选择一种我没有经验的语言或一种我经验很少的语言。另外,我想找到一些可以轻松编译为机器代码的东西,因为这是命令行工具的新增功能。
对我来说,第一语言对我来说很明显。这可能是因为我使用的许多命令行工具都是用Go编写的。但是我在Rust编程方面也有一点经验,在我看来,这种语言也非常适合我的项目。
考虑到Go和Rust,我认为您可以选择两种语言。由于我的主要目标是自学,因此此举将为我提供一个极好的机会来两次实施该项目,并独立找出每种语言的优缺点。
在这里,我想提一下Crystal和Nim两种语言。他们看起来很有前途。我期待着有机会在下一个项目中对其进行测试。
当地环境
在使用一套新工具之前,我总是对它的可用性感兴趣。即,是否必须使用某种程序包管理器在系统上全局安装程序。或者,对我来说,似乎更方便的解决方案是,是否可以根据用户帐户安装所有内容。我们谈论的是版本管理器,它们简化了我们的工作,侧重于在用户而非整个系统上安装程序。在Node.js环境中,NVM做得很好。
使用Go时,您可以将GVM用于相同的目的。该项目负责本地软件的安装和版本控制。安装非常简单:
gvm install go1.14 -B
gvm use go1.14
在Go中准备开发环境时,您需要注意两个环境变量-
GOROOT
和的存在GOPATH
。您可以在此处阅读有关它们的更多信息。
我使用Go遇到的第一个问题如下。当我试图了解模块解析系统如何工作以及如何应用时
GOPATH
,要建立一个具有功能性本地开发环境的项目结构是非常困难的。
我最终只是使用项目目录
GOPATH=$(pwd)
。这样做的主要好处是,我有一个可以处理依赖项的系统,受制于一个单独项目的框架,例如node_modules
。该系统运行良好。
在使用完工具之后,我发现有一个virtualgo项目可以帮助我解决的问题
GOPATH
。
Rust具有正式的rustup安装程序,该安装程序安装使用Rust所需的工具箱。可以用一个命令安装Rust。此外,在使用时
rustup
,我们可以访问其他组件,例如rls服务器和rustfmt代码格式化程序。许多项目需要每晚构建Rust工具箱。多亏了该应用程序rustup
,我才可以在版本之间进行切换。
编辑器支持
我正在使用VS Code,并且能够找到针对Go和Rust的扩展。两种语言在编辑器中均得到完美支持。
要调试Rust代码,请按照本教程进行操作,我需要安装CodeLLDB扩展名。
包装管理
Go生态系统没有程序包管理器,甚至没有官方注册表。在此,模块解析系统基于从外部URL导入模块的基础。
Rust使用Cargo软件包管理器来管理依赖关系,后者从官方Rust软件包注册表中的crates.io下载软件包。在生态系统包装箱中,可以将文件发布在docs.rs上。
图书馆
我探索新语言的第一个目标是弄清楚使用请求和变异与GraphQL服务器实现简单HTTP通信会有多困难。
说到Go,我设法找到了一些库,例如machinebox / graphql和shurcooL / graphql。第二种使用用于封送和拆封数据的结构。因此,我选择了她。
我需要在客户端上自定义标头时分叉shurcooL / graphql
Authorization
。更改由该PR提交。
这是调用Go中编写的GraphQL突变的示例:
type creationMutation struct {
CreateSession struct {
Token graphql.String
} `graphql:"createSession(email: $email, password: $password)"`
}
type CreationPayload struct {
Email string
Password string
}
func Create(client *graphql.Client, payload CreationPayload) (string, error) {
var mutation creationMutation
variables := map[string]interface{}{
"email": graphql.String(payload.Email),
"password": graphql.String(payload.Password),
}
err := client.Mutate(context.Background(), &mutation, variables)
return string(mutation.CreateSession.Token), err
}
使用Rust时,我需要使用两个库来执行GraphQL查询。这里的重点是该库
graphql_client
独立于协议,旨在生成用于序列化和反序列化数据的代码。因此,我需要第二个库(reqwest
),通过它我可以处理HTTP请求。
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/createSession.graphql"
)]
struct CreateSession;
pub struct Session {
pub token: String,
}
pub type Creation = create_session::Variables;
pub async fn create(context: &Context, creation: Creation) -> Result<Session, api::Error> {
let res = api::build_base_request(context)
.json(&CreateSession::build_query(creation))
.send()
.await?
.json::<Response<create_session::ResponseData>>()
.await?;
match res.data {
Some(data) => Ok(Session {
token: data.create_session.token,
}),
_ => Err(api::Error(api::get_error_message(res).to_string())),
}
}
Go和Rust库均未通过WebSocket协议支持GraphQL。
实际上,该库
graphql_client
支持订阅,但是由于它是独立于协议的,因此我必须自己使用GraphQL实现WebSocket交互机制。
要在应用程序的Go版本中使用WebSocket,必须修改该库。由于我已经使用了库的fork,所以我不想这样做。相反,我使用了一种“观看”新推文的简化方法。即,为了接收推文,我每5秒向API发送一次请求。我不感到自豪,我也只是那个。
在Go中编写程序时,可以使用关键字
go
运行称为goroutines的轻量级流。Rust使用操作系统线程,这是通过调用来完成的Thread::spawn
。通道用于在流之间以及在那里和那里之间传输数据。
错误处理
Go与其他任何值一样对待错误。在Go中处理错误的通常方法是检查错误:
func (config *Config) Save() error {
contents, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(config.path, contents, 0o644)
if err != nil {
return err
}
return nil
}
Rust的枚举
Result<T, E>
包含指示成功或失败的值。分别是Ok(T)
和Err(E)
。这里还有另一个枚举Option<T>
,包括值Some(T)
和None
。如果您熟悉Haskell,则可以识别这些monadEither
并具有这些含义Maybe
。
还有一个与错误传播(运算符
?
)有关的“语法糖” ,它解析结构的值,Result
或者Option
自动返回,Err(...)
或者None
在出现问题时自动返回。
pub fn save(&mut self) -> io::Result<()> {
let json = serde_json::to_string(&self.contents)?;
let mut file = File::create(&self.path)?;
file.write_all(json.as_bytes())
}
此代码等效于以下代码:
pub fn save(&mut self) -> io::Result<()> {
let json = match serde_json::to_string(&self.contents) {
Ok(json) => json,
Err(e) => return Err(e.into())
};
let mut file = match File::create(&self.path) {
Ok(file) => file,
Err(e) => return Err(e.into())
};
file.write_all(json.as_bytes())
}
因此,Rust具有以下内容:
- 单子结构(
Option
和Result
)。 - 操作员支持
?
。 - 一种特征,
From
用于在错误传播时自动对其进行转换。
以上三个功能的结合为我们提供了一个错误处理系统,我称之为最好的错误处理系统。它既简单又精简,使用它编写的代码也易于维护。
编译时间
Go是一种以其编写的代码将尽快编译的想法创建的语言。让我们研究这个问题:
> time go get hashtrack #
go get hashtrack 1,39s user 0,41s system 43% cpu 4,122 total
> time go build -o hashtrack hashtrack #
go build -o hashtrack hashtrack 0,80s user 0,12s system 152% cpu 0,603 total
> time go build -o hashtrack hashtrack #
go build -o hashtrack hashtrack 0,19s user 0,07s system 400% cpu 0,065 total
> time go build -o hashtrack hashtrack #
go build -o hashtrack hashtrack 0,94s user 0,13s system 169% cpu 0,629 total
令人印象深刻。现在,让我们看看Rust将向我们展示什么:
> time cargo build
Compiling libc v0.2.67
Compiling cfg-if v0.1.10
Compiling autocfg v1.0.0
...
...
...
Compiling hashtrack v0.1.0 (/home/paulo/code/cuchi/hashtrack/cli-rust)
Finished dev [unoptimized + debuginfo] target(s) in 1m 44s
cargo build 363,80s user 17,05s system 365% cpu 1:44,09 total
所有依赖项均在此处编译,共214个模块。当您重新开始编译时,一切都已经准备就绪,因此几乎可以立即执行此任务:
> time cargo build #
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
cargo build 0,07s user 0,03s system 104% cpu 0,094 total
> time cargo build #
Compiling hashtrack v0.1.0 (/home/paulo/code/cuchi/hashtrack/cli-rust)
Finished dev [unoptimized + debuginfo] target(s) in 3.15s
cargo build 3,01s user 0,52s system 111% cpu 3,162 total
如您所见,Rust使用增量编译模型。从修改后的模块开始到依赖于它的模块结束,将对依赖项树进行部分重新编译。
由于在这种情况下编译器会优化代码,因此项目的发布版本将花费更多的时间,这是可以预期的。
> time cargo build --release
Compiling libc v0.2.67
Compiling cfg-if v0.1.10
Compiling autocfg v1.0.0
...
...
...
Compiling hashtrack v0.1.0 (/home/paulo/code/cuchi/hashtrack/cli-rust)
Finished release [optimized] target(s) in 2m 42s
cargo build --release 1067,72s user 16,95s system 667% cpu 2:42,45 total
持续集成
我们在上面确定的用Go和Rust编写的编译项目的功能在持续集成系统中很有希望出现。
进行项目处理
处理Rust项目
内存消耗
为了分析不同版本的命令行工具的内存消耗,我使用了以下命令:
/usr/bin/time -v ./hashtrack list
该命令
time -v
显示了很多有趣的信息,但是我对进程指示器感兴趣,进程指示器Maximum resident set size
是在程序执行期间分配给程序的物理内存的峰值。
这是我用于收集程序不同版本的内存消耗数据的代码:
for n in {1..5}; do
/usr/bin/time -v ./hashtrack list > /dev/null 2>> time.log
done
grep 'Maximum resident set size' time.log
这是Go版本的结果:
Maximum resident set size (kbytes): 13632
Maximum resident set size (kbytes): 14016
Maximum resident set size (kbytes): 14244
Maximum resident set size (kbytes): 13648
Maximum resident set size (kbytes): 14500
这是程序的Rust版本的内存消耗:
Maximum resident set size (kbytes): 9840
Maximum resident set size (kbytes): 10068
Maximum resident set size (kbytes): 9972
Maximum resident set size (kbytes): 10032
Maximum resident set size (kbytes): 10072
该内存是在以下任务期间分配的:
- 系统参数的解释。
- 从文件系统加载和解析配置文件。
- 使用TLS通过HTTP访问GraphQL。
- 解析JSON响应。
- 将格式化的数据写入
stdout
。
Go和Rust具有不同的内存管理方式。
Go有一个垃圾收集器,用于检测未使用的内存并将其回收。结果,程序员不会因这些任务而分心。由于垃圾收集器基于启发式算法,因此使用垃圾收集器总是意味着要做出让步。通常在性能和应用程序使用的内存量之间。
Rust的内存管理模型具有所有权,借用,生存期等概念。这不仅有助于安全的内存管理,而且可以确保完全控制堆上分配的内存,而无需手动进行内存管理或垃圾回收。
为了进行比较,让我们看一下解决与我的问题相似的其他程序。
命令 | 最大居民集大小(千字节) |
heroku apps |
56436 |
gh pr list |
26456 |
git ls-remote (具有SSH访问权限) |
6448 |
git ls-remote (具有HTTP访问权限) |
23488 |
我选择Go的原因
我出于某些原因会选择Go,原因如下:
- 如果我需要一种易于团队成员学习的语言。
- 如果我想编写简单的代码,但要牺牲语言的灵活性。
- 如果我只为Linux开发软件,或者Linux是我最感兴趣的操作系统。
- 如果项目的编译时间很重要。
- 如果我需要成熟的机制来执行异步代码。
我选择Rust的原因
以下是可能导致我为项目选择Rust的原因:
- 如果我需要高级错误处理系统。
- 如果我想用一种多范式语言编写,这使我可以编写比其他语言创建的更具表达力的代码。
- 如果我的项目对安全性有很高的要求。
- 高性能对项目是否至关重要。
- 如果该项目针对许多操作系统,并且我想拥有一个真正的多平台代码库。
一般说明
Go和Rust仍然有些怪癖困扰着我。这些是以下内容:
- 转到如此集中,有时这种追求具有相反的效果(例如,如在与箱子简单
GOROOT
和GOPATH
)。 - 我仍然不太了解Rust中的“终身”概念。甚至尝试使用相应的语言机制也使我失去了平衡。
是的,我想指出的是,在新版本的Go中,使用它
GOPATH
不再会导致问题,因此我应该将项目转移到新版本的Go中。
我可以说Go和Rust都是非常有趣的语言。我发现它们是C / C ++编程世界的强大补充。它们使您可以创建多种用途的应用程序。例如,Web服务,甚至感谢WebAssembly,客户端Web应用程序。
结果
Go和Rust是非常适合开发命令行工具的出色工具。但是,当然,他们的创作者受到不同优先级的指导。一种语言旨在使软件开发变得简单且易于访问,从而可以维护用该语言编写的代码。另一种语言的优先级是合理性,安全性和性能。
如果您想了解更多关于去锈VS比较,看看这个文章。除其他外,它引起了有关程序的多平台兼容性的严重问题。
您将使用哪种语言来开发命令行工具?