Go vs Rust: Writing a CLI tool

栏目: IT技术 · 发布时间: 4年前

内容简介:This text is about my adventure writing a small CLI application (twice) using two languages I had little experience with.If you are eager to jump right into the code and compare it yourself, check it out the

Go vs Rust: Writing a CLI tool

This text is about my adventure writing a small CLI application (twice) using two languages I had little experience with.

If you are eager to jump right into the code and compare it yourself, check it out the Go source and the Rust source .

About the Project

I have a pet project called Hashtrack, which is a full-stack web application I wrote for a technical interview. This project is rather small and it is simple to use:

  1. You authenticate - considering you already created your account
  2. You input hashtags you want to track
  3. You wait for the captured tweets to show on your screen

Check it out here.

After my interview, I kept improving this project just for fun, and I noticed that it could be a perfect place to test my skills by implementing a CLI tool. I already had the server, so I just needed to pick a language to implement a small set of features under my project's API.

Features

hashtrack login
hashtrack logout
hashtrack track <hashtag> [...]
hashtrack untrack <hashtag> [...]
hashtrack tracks
hashtrack list
hashtrack watch
hashtrack status
--endpoint
--config
endpoint

What we have to know beforehand:

  • The CLI should use the project's API, which is GraphQL under HTTP + WebSockets.
  • The CLI should use the filesystem to store a config file.
  • The CLI should parse positional arguments and flags.

How did I end up using Go and Rust?

There is a large set of languages you can use to write CLI tools.

In this case, I wanted a language I had little or no prior experience with, I also wanted one that could easily compile to a native executable, which is a nice perk to have on a CLI tool.

My first obvious choice was Go, for some reason. But I also had little experience with Rust, and I saw it could also be a good fit for this project.

So... why not both? Since my main objective here is to learn, could be a great opportunity to implement this project twice and find what are the pros and cons of each one from my point of view.

Honorable mentions to Crystal and Nim , those were very promising options too. I'm looking forward to learn about them in another pet project.

Local environment

The first thing I look when using a new toolset is whether it has an easy way to make it available for my user, without using the distribution package manager to install it system-wide. We are talking about version managers, they make our life easier by installing the tools in a user-wide manner instead of system-wide. NVM for Node.js does it very well.

When using Go, there is the GVM project which handles the local install & version management, and it is easy to setup:

gvm install go1.14 -B
gvm use go1.14

There are also two environment variables we need to know, they are GOROOT and GOPATH -- You can read more about them here .

The first problem I found using Go, was when I was figuring out how the module resolution worked along with the GOPATH , it became quite frustrating to set up a project structure with a functional local development environment.

In the end, I just used GOPATH=$(pwd) in my project's directory, the main perk was to have a per-project dependency setup, like a node_modules . It worked well.

After finishing my project, I found out that virtualgo existed and would solve my problems with GOPATH .

Rust has an official project called rustup , which manages the Rust installation, also known as toolchain . It can be easily set up with a one-liner. Also, there is a set of optional components using rustup , such as the rls and rustfmt . Many projects require a nightly version of the Rust toolchain, with rustup there was no problem switching between the versions.

Editor Support

For both of the languages, editor tooling was flawless, as a VSCode user, I can find extensions for both Go and Rust in the marketplace.

When debugging with Rust, I had to install the CodeLLDB extension after following this tutorial .

Package management

Go doesn't have a package manager or even an official registry. Instead, its module resolution works in a way you can import them from external URLs.

For dependency management, Rust uses the Cargo, which downloads and compiles dependencies from crates.io , which is the official registry for Rust packages. Packages inside the Crates ecosystem can also have their documentation available in docs.rs

Libraries

My first objective was to see how easy could be to implement a simple GraphQL query/mutation over HTTP.

For the Go language, I found some libraries, like machinebox/graphql and shurcooL/graphql , the second one uses structs for (un) marshaling the data, that is what made me stick to it.

I used a fork of shurcooL/graphql, because I needed to set the Authorization header in the client, the changes are in this pull request .

This is the Go example of an raphQL mutation call:

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
}

In Rust, I had to use two libraries to make GraphQL calls. That is because graphql_client is protocol-agnostic, it only focuses on code generation for serializing and deserializing data. So I needed a second library ( reqwest ) to take care of the HTTP requests.

#[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())),
    }
}

Neither of the libraries for Go and Rust had any implementation for GraphQL via WebSocket protocol.

In fact, graphql_client for Rust supports Subscriptions , but since it is protocol-agnostic, I had to implement the whole GraphQL WebSocket communication on my own, check it out .

To use WebSockets in the Go version, the library should be modified to support the protocol. Since I was already using a fork of the library, I didn't feel like doing it. Instead, I used a poor man's way of "watching" the new tweets, which was to request the API every 5 seconds to retrieve them, I'm not proud of it .

Using Go, there is the go keyword to spawn a lightweight thread, also called goroutine . In contrast, Rust uses operating system threads by calling a Thread::spawn . Besides that, both implementations use channels to transfer objects between their threads.

Error handling

In Go, errors are treated just like any other value. The common way to handle errors in Go is to just check if they are present.

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 has the Result<T, E> enum, which can encapsulate an Ok(T) for success, or an Err(E) for errors. It also has the Option<T> enum, with Some(T) or None . If you are familiar with Haskell, you may recognize those as the Either and the Maybe monads.

There is also a syntactic sugar for error propagation (the ? operator) that resolves the value from the Result or Option structure, automatically returning Err(...) or None when something goes bad.

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())
}

The code above is the equivalent of

pub fn save(&mut self) -> io::Result<()> {
    let json = match serde_json::to_string(&self.contents) {
        Ok(json) => json,
        Err(e) => return Err(e)
    };
    let mut file = match File::create(&self.path) {
        Ok(file) => file,
        Err(e) => return Err(e)
    };
    file.write_all(json.as_bytes())
}

Rust has:

  • monadic constructs ( Option & Result )
  • the error propagation operator
  • the From trait, to automatically convert errors on propagation

The combination of the three features above makes up the best error handling solution I saw in a language, being simple, sound, and maintainable at the same time.

Compilation time

Go is built with fast compilation time as a critical requirement, let's see:

> time go get hashtrack # Install dependencies
go get hashtrack  1,39s user 0,41s system 43% cpu 4,122 total

> time go build -o hashtrack hashtrack # First time
go build -o hashtrack hashtrack  0,80s user 0,12s system 152% cpu 0,603 total

> time go build -o hashtrack hashtrack # Second time
go build -o hashtrack hashtrack  0,19s user 0,07s system 400% cpu 0,065 total

> time go build -o hashtrack hashtrack # Made a change
go build -o hashtrack hashtrack  0,94s user 0,13s system 169% cpu 0,629 total

That's impressive, let's see how Rust does this:

> 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

It compiled all the dependencies, which are 214 modules in total. When we run it again, everything is already compiled, so it runs instantly:

> time cargo build # Second time
    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 # Made a change
   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

As you can see, Rust uses an incremental compilation model, which partially recompiles the module dependency tree, starting from changed modules until it propagates into its dependents.

If you are doing a release build, it takes longer, which is expected because of the optimization tasks the compiler do internally:

> 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

Continuous Integration

As you would expect, the time differences show up on the CI workflow: Go vs Rust: Writing a CLI tool Go vs Rust: Writing a CLI tool

Memory usage

To measure memory usage, I used /usr/bin/time -v ./hashtrack list for each of the versions. time -v displays a lot of interesting info, but here we are looking for the Maximum resident set size of the process, which is the peak amount of allocated physical memory during the execution.

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

This memory usage accounts for the task of:

stdout

Both languages have different ways to manage memory and allocations.

Go has a garbage collector, which is a common way to track down unused heap memory and reclaim it instead of doing this manually. Since garbage collectors are a composition of heuristics, there are always tradeoffs, generally between performance and memory usage.

Rust memory model has concepts like ownership , borrowing , and lifetimes , which not only helps with memory safety, but also guarantee total control of the program heap memory without manual management or a garbage collector.

For comparison, let's take some other executables which do a rather similar task:

Command Maximum resident set size (kbytes)
heroku apps 56436
gh pr list 26456
git ls-remote (With a SSH remote) 6448
git ls-remote (With a HTTPS remote) 23488

Conclusion

They were both very great tools for the job. But of course, they have different priorities. On one side, we have an option which tries to keep software development simple, maintainable, and accessible. On the other hand, we have a language focused on soundness, safety, and performance.

Reasons I would use Go

  • I want a very simple language for my teammates to learn
  • I want little flexibility, to write plain and simple code
  • If I build exceptionally/mostly for Linux
  • If compilation time is an issue
  • I want mature asynchronous semantics

Reasons I would use Rust

  • I want state-of-the-art error handling for my code
  • I want a multi-paradigm language which lets me write more expressive code
  • If the project has critical requirements about security
  • If the project has critical requirements about performance
  • If the project targets many operating systems and I want a truly multiplatform codebase

There are some details from both of the languages that still triggers me.

  • Go focus so much on being simple that it has the opposite effect sometimes (like GOROOT and GOPATH , for example).
  • I still don't understand very well how lifetimes work in Rust, and it can get quite frustrating if you ever try to deal with it.

From a personal perspective, both were very fun to learn, and are a great addition in a world of C and C++. They provide a broader range of applications, like web services and even front-end web frameworks , thanks to WebAssembly :)

If you want another comparison between the two languages that is far in-depth than this one, check out this article from fasterthanlime .


以上所述就是小编给大家介绍的《Go vs Rust: Writing a CLI tool》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

离散数学及其应用

离散数学及其应用

Kenneth H.Rosen / 机械工业出版社 / 2012-11 / 99.00元

本书是介绍离散数学理论和方法的经典教材,已经成为采用率最高的离散数学教材,被美国众多名校用作教材,获得了极大的成功。中文版也已被国内大学广泛采用为教材。作者参考使用教师和学生的反馈,并结合自身对教育的洞察,对第7版做了大量的改进,使其成为更有效的教学工具。. 本书可作为1至2个学期的离散数学课入门教材,适用于数学,计算机科学。计算机工程.信息技术等专业的学生。 本书特点 实例:书中有8......一起来看看 《离散数学及其应用》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具