blog: start tl;dr rust post
This commit is contained in:
parent
09e1d478df
commit
0677fda161
|
@ -0,0 +1,536 @@
|
|||
---
|
||||
title: "TL;DR Rust"
|
||||
date: 2020-09-19
|
||||
tags:
|
||||
- rust
|
||||
- go
|
||||
- golang
|
||||
---
|
||||
|
||||
# TL;DR Rust
|
||||
|
||||
Recently I've been starting to use Rust more and more for larger and larger
|
||||
projects. As things have come up, I realized that I am missing a good reference
|
||||
for common things in Rust as compared to Go. This post contains a quick
|
||||
high-level overview of patterns in Rust and how they compare to patterns
|
||||
in Go. This will focus on code samples. This is no replacement for the [Rust
|
||||
book](https://doc.rust-lang.org/book/), but should help you get spun up on the
|
||||
various patterns used in Rust code.
|
||||
|
||||
Also I'm happy to introduce Mara to the blog!
|
||||
|
||||
[Hey, happy to be here! I'm Mara, I'll interject with side information,
|
||||
challenge assertions and more! Thanks for inviting
|
||||
me!](conversation://Mara/hacker)
|
||||
|
||||
Let's start somewhere simple: functions.
|
||||
|
||||
## Making Functions
|
||||
|
||||
Functions are defined using `fn` instead of `func`:
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
func foo() {}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn foo() {}
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
Arguments can be passed by separating the name from the type with a colon:
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
func foo(bar int) {}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn foo(bar: i32) {}
|
||||
```
|
||||
|
||||
### Returns
|
||||
|
||||
Values can be returned by adding `-> Type` to the function declaration:
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
func foo() int {
|
||||
return 2
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn foo() -> i32 {
|
||||
return 2;
|
||||
}
|
||||
```
|
||||
|
||||
In Rust values can also be returned on the last statement without the `return`
|
||||
keyword or a terminating semicolon:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn foo() -> i32 {
|
||||
2
|
||||
}
|
||||
```
|
||||
|
||||
### Functions that can fail
|
||||
|
||||
The [Result](https://doc.rust-lang.org/std/result/) type represents things that
|
||||
can fail with specific errors. The [eyre Result
|
||||
type](https://docs.rs/eyre) represents things that can fail
|
||||
with any error. For readability, this post will use the eyre Result type.
|
||||
|
||||
[The angle brackets in the `Result` type are arguments to the type, this allows
|
||||
the Result type to work across any type you could imagine.](conversation://Mara/hacker)
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
import "errors"
|
||||
|
||||
func divide(x, y int) (int, err) {
|
||||
if y == 0 {
|
||||
return 0, errors.New("cannot divide by zero")
|
||||
}
|
||||
|
||||
return x / y, nil
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
use eyre::{eyre, Result};
|
||||
|
||||
fn divide(x: i32, y: i32) -> Result<i32> {
|
||||
match y {
|
||||
0 => Err(eyre!("cannot divide by zero")),
|
||||
_ => Ok(x / y),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[Huh? I thought Rust had the <a
|
||||
href="https://doc.rust-lang.org/std/error/trait.Error.html">Error trait</a>,
|
||||
shouldn't you be able to use that instead of a third party package like
|
||||
eyre?](conversation://Mara/wat)
|
||||
|
||||
Let's try that, however we will need to make our own error type because the
|
||||
[`eyre!`](https://docs.rs/eyre/0.6.0/eyre/macro.eyre.html) macro creates its own
|
||||
transient error type on the fly.
|
||||
|
||||
First we need to make our own simple error type for a DivideByZero error:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DivideByZero;
|
||||
|
||||
impl fmt::Display for DivideByZero {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "cannot divide by zero")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for DivideByZero {}
|
||||
```
|
||||
|
||||
So now let's use it:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn divide(x: i32, y: i32) -> Result<i32, DivideByZero> {
|
||||
match y {
|
||||
0 => Err(DivideByZero{}),
|
||||
_ => Ok(x / y),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However there is still one thing left: the function returns a DivideByZero
|
||||
error, not _any_ error like the [error interface in
|
||||
Go](https://godoc.org/builtin#error). In order to represent that we need to
|
||||
return something that implements the Error trait:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
fn divide(x: i32, y: i32) -> Result<i32, impl Error> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
And for the simple case, this will work. However as things get more complicated
|
||||
this simple facade will not work due to reality and its complexities. This is
|
||||
why I am shipping as much as I can out to other packages like eyre or
|
||||
[anyhow](https://docs.rs/anyhow). Check out this code in the [Rust
|
||||
Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=946057d8eb02f388cb3f03bae226d10d)
|
||||
to mess with this code interactively.
|
||||
|
||||
[Pro tip: eyre (via <a href="https://docs.rs/color-eyre">color-eyre</a>) also
|
||||
has support for adding <a href="https://docs.rs/color-eyre/0.5.4/color_eyre/#custom-sections-for-error-reports-via-help-trait">custom
|
||||
sections and context</a> to errors similar to Go's <a href="https://godoc.org/fmt#Errorf">`fmt.Errorf` `%w`
|
||||
format argument</a>, which will help in real world
|
||||
applications. When you do need to actually make your own errors, you may want to look into
|
||||
crates like <a href="https://docs.rs/thiserror">thiserror</a> to help with
|
||||
automatically generating your error implementation.](conversation://Mara/hacker)
|
||||
|
||||
### The `?` Operator
|
||||
|
||||
In Rust, the `?` operator checks for an error in a function call and if there is
|
||||
one, it automatically returns the error and gives you the result of the function
|
||||
if there was no error. This only works in functions that return a Result.
|
||||
|
||||
```go
|
||||
// go
|
||||
|
||||
func doThing() (int, error) {
|
||||
result, err := divide(3, 4)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
use eyre::Result;
|
||||
|
||||
fn do_thing() -> Result<i32> {
|
||||
let result = divide(3, 4)?;
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
If the second argument of divide is changed to `0`, then `do_thing` will return
|
||||
an error.
|
||||
|
||||
[And how does that work with eyre?](conversation://Mara/hmm)
|
||||
|
||||
It works with eyre because eyre has its own error wrapper type called
|
||||
[`Report`](https://docs.rs/eyre/0.6.0/eyre/struct.Report.html), which can
|
||||
represent anything that implements the Error trait.
|
||||
|
||||
## Macros
|
||||
|
||||
Rust macros are function calls with `!` after their name:
|
||||
|
||||
```rust
|
||||
// rust
|
||||
|
||||
println!("hello, world");
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
Variables are created using `let`:
|
||||
|
||||
```go
|
||||
// go
|
||||
var foo int
|
||||
var foo = 3
|
||||
foo := 3
|
||||
```
|
||||
|
||||
```rust
|
||||
// rust
|
||||
let foo: i32;
|
||||
let foo = 3;
|
||||
```
|
||||
|
||||
### Mutability
|
||||
|
||||
In Rust, every variable is immutable (unchangeable) by default. To create a
|
||||
mutable variable, add the `mut` keyword after the `let` keyword. There is no
|
||||
analog to this in Go.
|
||||
|
||||
```rust
|
||||
let mut i: i32 = 0;
|
||||
i = i + 5;
|
||||
```
|
||||
|
||||
[This is slightly a lie. There's more advanced cases involving interior
|
||||
mutability and other fun stuff like that, however this is a more advanced topic
|
||||
that isn't covered here.](conversation://Mara/hacker)
|
||||
|
||||
### Lifetimes
|
||||
|
||||
Rust does garbage collection at compile time. It also passes ownership of memory
|
||||
to functions as soon as possible. For example:
|
||||
|
||||
```rust
|
||||
let quo = divide(4, 8)?;
|
||||
let other_quo = divide(quo, 5)?;
|
||||
|
||||
// Fails compile because quo was given to divide to create other_quo
|
||||
let yet_another_quo = divide(quo, 4)?;
|
||||
```
|
||||
|
||||
To work around this you need to either clone the value or pass a reference:
|
||||
|
||||
```rust
|
||||
let other_quo = divide(quo.clone(), 5);
|
||||
let yet_another_quo = divide(quo, 4)?;
|
||||
```
|
||||
|
||||
To pass a reference to a function, use the `&` character:
|
||||
|
||||
```
|
||||
let something = do_something(&quo)?;
|
||||
```
|
||||
|
||||
### Passing Mutability
|
||||
|
||||
Sometimes functions need mutable variables. To pass a mutable reference, add
|
||||
`&mut` before the name of the variable:
|
||||
|
||||
```rust
|
||||
let something = do_something_to_quo(&mut quo)?;
|
||||
```
|
||||
|
||||
## Project Setup
|
||||
|
||||
### Imports
|
||||
|
||||
External dependencies are declared using the [Cargo.toml
|
||||
file](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html):
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
```
|
||||
|
||||
This depends on the crate [anyhow](https://crates.io/anyhow) at version 1.0.x.
|
||||
|
||||
Dependencies can also have optional features:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
reqwest = { version = "0.10", features = ["json"] }
|
||||
```
|
||||
|
||||
This depends on the crate [reqwest](https://crates.io/reqwest) at version 0.10.x
|
||||
with the `json` feature enabled (in this case it enables reqwest being able to
|
||||
automagically convert things to/from json using Serde).
|
||||
|
||||
External dependencies can be used with the `use` statement:
|
||||
|
||||
```go
|
||||
import "github.com/foo/bar"
|
||||
```
|
||||
|
||||
```rust
|
||||
use foo; // -> foo now has the members of crate foo behind the :: operator
|
||||
use foo::Bar; // -> Bar is now exposed as a type in this file
|
||||
|
||||
use anyhow::{anyhow, Result}; // exposes the anyhow! and Result members of anyhow
|
||||
```
|
||||
|
||||
## Async/Await
|
||||
|
||||
Async functions may be interrupted to let other things execute as needed. This
|
||||
program uses [tokio](https://tokio.rs/) to handle async tasks. To run an async
|
||||
task and wait for its result, do this:
|
||||
|
||||
```
|
||||
let response = reqwest::get("https://printerfacts.cetacean.club/fact")
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
```
|
||||
|
||||
This will populate `response` with the HTML source of https://within.website.
|
||||
|
||||
To make an async function, add the `async` keyword before the `fn` keyword:
|
||||
|
||||
```rust
|
||||
async fn get_html(url: String) -> Result<String> {
|
||||
reqwest::get(&url)
|
||||
.await?
|
||||
.text()
|
||||
.await?
|
||||
}
|
||||
```
|
||||
|
||||
This can then be called like this:
|
||||
|
||||
```rust
|
||||
let within_website_html = get_html("https://within.website").await?;
|
||||
```
|
||||
|
||||
## Public/Private Types and Functions
|
||||
|
||||
Rust has three privacy levels for functions:
|
||||
|
||||
- Only visible to the current file (no keyword, lowercase in Go)
|
||||
- Visible to anything in the current crate (`pub(crate)`, internal packages in
|
||||
go)
|
||||
- Visible to everyone (`pub`, upper case in Go)
|
||||
|
||||
This project will mostly use `pub(crate)` as none of this code is intended to be
|
||||
consumed by other programs (though this may change in the future).
|
||||
|
||||
## Structures
|
||||
|
||||
Rust structures are created using the `struct` keyword:
|
||||
|
||||
```go
|
||||
type Client struct {
|
||||
Token string
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
pub(crate) struct Client {
|
||||
pub token: String,
|
||||
}
|
||||
```
|
||||
|
||||
If the `pub` keyword is not specified before a member name, it will not be
|
||||
usable outside the Rust source code file it is defined in:
|
||||
|
||||
```go
|
||||
type Client struct {
|
||||
token string
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
pub(crate) struct Client {
|
||||
token: String,
|
||||
}
|
||||
```
|
||||
|
||||
### Encoding structs to JSON
|
||||
|
||||
[serde](https://serde.rs) is used to convert structures to json. The Rust
|
||||
compiler's
|
||||
[derive](https://doc.rust-lang.org/stable/rust-by-example/trait/derive.html)
|
||||
feature is used to automatically implement the conversion logic.
|
||||
|
||||
```go
|
||||
type Response struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct Response {
|
||||
pub name: String,
|
||||
pub description: Option<String>, // Option means that there can either be something or nothing there
|
||||
}
|
||||
```
|
||||
|
||||
## Strings
|
||||
|
||||
Rust has a few string types that do different things. You can read more about
|
||||
this [here](https://fasterthanli.me/blog/2020/working-with-strings-in-rust/),
|
||||
but at a high level this project only uses two of them:
|
||||
|
||||
- String, an owned UTF-8 string
|
||||
- PathBuf, a filepath string (encoded in whatever encoding the OS running this
|
||||
code uses for filesystems)
|
||||
|
||||
The strings are different types for safety reasons. See the linked blogpost for
|
||||
more detail about this.
|
||||
|
||||
## Enumerations / Tagged Unions
|
||||
|
||||
Enumerations, also known as tagged unions, are a way to specify a superposition
|
||||
of one of a few different kinds of values in one type. The main place they are
|
||||
used in this project is for command line parsing with
|
||||
[structopt](https://docs.rs/structopt/0.3.14/structopt/). There is no easy
|
||||
analog for this in Go.
|
||||
|
||||
```rust
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(about = "A simple release management tool")]
|
||||
pub(crate) enum Cmd {
|
||||
/// Creates a new release for a git repo
|
||||
Cut {
|
||||
#[structopt(flatten)]
|
||||
common: Common,
|
||||
/// Changelog location
|
||||
#[structopt(long, short, default_value="./CHANGELOG.md")]
|
||||
changelog: PathBuf,
|
||||
},
|
||||
|
||||
/// Runs releases as triggered by GitHub Actions
|
||||
GitHubAction {
|
||||
#[structopt(flatten)]
|
||||
gha: GitHubAction,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Enum variants can be matched using the `match` keyword:
|
||||
|
||||
```rust
|
||||
match cmd {
|
||||
Cmd::Cut { common, changelog } => {
|
||||
cmd::cut::run(common, changelog).await
|
||||
}
|
||||
Cmd::GitHubAction { gha } => {
|
||||
cmd::github_action::run(gha).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All variants of an enum must be matched in order for the code to compile.
|
||||
|
||||
## Testing
|
||||
|
||||
Test functions need to be marked with the `#[test]` annotation, then they will
|
||||
be run alongside `cargo test`:
|
||||
|
||||
```rust
|
||||
mod tests { // not required but it is good practice
|
||||
#[test]
|
||||
fn math_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn http_works() {
|
||||
let _ = get_html("https://within.website").await.unwrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Avoid the use of `unwrap()` outside of tests. In the wrong cases, using
|
||||
`unwrap()` in production code can cause the server to crash and can incur data
|
||||
loss.
|
||||
|
||||
---
|
||||
|
||||
This is by no means comprehensive, see the rust book or [Learn X in Y Minutes
|
||||
Where y = Rust](https://learnxinyminutes.com/docs/rust/) for more information.
|
||||
This code is written to be as boring and obvious as possible. If things don't
|
||||
make sense, please reach out and don't be afraid to ask questions.
|
Loading…
Reference in New Issue