The Go Developer's Quickstart Guide to Rust
You've been writing Go. But you're feeling an urge to test the waters with Rust. This is a guide to make this switch easy.
As the co-author of Go in Practice, I have felt a certain obligation to Go. But I'm ready for a change. Rust topped the satisfaction survey in Stack Overflow's survey of languages (screenshot above). I've decided to give it a try. While Go and Rust are often compared, they are remarkably different languages.
Coming from a Go background, there are things about Rust that feel very natural, and things (like memory management) that feel utterly foreign. And so as I learn Rust, I am cataloging how it feels for a Go programmer. And rather than leading others to "dive in at the deep end" as I did (when I tried to write a full web service), I decided to approach Rust by starting with similarities and working toward differences.
The Rust Toolchain
The first place to start is with the Rust toolchain. Like Go, Rust (the core distribution) ships with a veritable cornucopia of tooling. Unlike Go, Rust considers package management essential, so its tooling is actually much easier to learn than Go's.
To get started, install Rust as recommended. Don't get fancy. Don't try to find alternative installers. The rustup
tool is simple and broadly used. And, to borrow a popular Go-ism, it's the "idiomatic" way of working with Rust. Later, you can get fancy if you want.
The rustup
tool will try to make all of the necessary path changes and set the necessary environment variables. If you follow the instructions, by the end of the setup, you should be able to open a new terminal and type which cargo
and have it print out a path.
If you go through Rust tutorials, they will (like Go tutorials), walk you through tools that you are unlikely to use directly in your day-to-day. I'll skip that and make one bold claim:
The tool you care about in the Rust world is
cargo
.
You'll use it to build, you'll use it for dependency management, you'll use it to debug, and you'll use it to release. Yeah, you'll probably need rustc
to debug something someday. But not today.
Create a Project
First off, Rust has no equivalent of $GOPATH
. Rust programs can go wherever you want. Second, you don't need to follow any particular pattern in path naming (though cargo
will create you an idiomatic package structure).
So cd
to wherever you like to store your code, and run this:
$ cargo new --bin hello
Created binary (application) `hello` project
Now you'll have a directory named hello
, and it will have a src/
directory and a Cargo.toml
.
The Cargo.toml
will look a lot like Dep's Gopkg.toml
(which is very, very unsurprising, given that Dep was heavily influenced by Cargo). If you're used to Glide, Dep, or Godep, Cargo tracks dependencies like these tools. If you're used to the idiomatic "winging it" method of go get
, then Cargo.toml
is the thing that keeps you from having to journey through dependency hell every time someone updates their code. Welcome to a better life!
Later, we'll add dependencies. For now, we'll start with a quick translation of a Go program to a Rust program.
Hello Go... errr... Rust
Let's start with the Go program from golang.org:
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
For now, put this in src/main.go
, then run your trusty old go run
command:
$ go run src/main.go
Hello, 世界
Okay, let's delete a some stuff and make this into a Rust program. First, copy the program into main.rs
(which should have been created for you).
Then do the following things:
- remove the
package
line and theimport
line - change
func
tofn
- change
fmt.Println
toprintln!
- add a semicolon at the end of the
println!
line
So your program should now look like this:
fn main() {
println!("Hello, 世界");
}
Other than the obvious, there are four things to be learned from the code above:
- Rust doesn't require explicit package names for some things (see Rust's
mod
for modules). - Semicolons are usually required (whereas in Go they are almost always optional). Pro-tip: Most of my first-run compile errors are a result of forgotten semicolons.
- Rust has macros, of which
println!
is one that is built in. - At the moment, the consensus in the Rust community is that developers should use spaces, not tabs, with a 4-space indent. (Yes, there's a
rustfmt
, and yes, you typically run it withcargo fmt
)
Now execute cargo run
:
$ cargo run
Compiling hello v0.1.0 (file:///Users/mbutcher/Code/Rust/hello)
Finished dev [unoptimized + debuginfo] target(s) in 2.1 secs
Running `target/debug/hello`
Hello, 世界
The cargo run
command compiles and executes your program. If you look at the contents of your hello
directory, you will notice that during the compilation phase, cargo
just added a Cargo.lock
and a target/
directory.
The Cargo.lock
file performs the same essential feature as Gopkg.lock
or Glide.lock
in Go programs.
It is idiomatic to track Cargo.lock in a VCS only for executables, but omitted for libraries.
The target/
directory will contain all your compiled goodies, organized by deployment type (debug, release, ...). So if we look in target/debug
, we will see our hello
binary.
Okay, we've just done the basics. Now let's dive in a bit more.
Using libraries, variables, and print macros
Ultimately, we want to build a program that goes from the familiar "Hello world" program to one that says "Good morning, world!" or "Good day, world!", depending on the time. But we'll take a shorter step first.
Let's change our program to print out the time. This will give us a glimpse into a few important aspects of Rust, such as using libraries.
use std::time::SystemTime;
fn main() {
let now = SystemTime::now();
println!("It is now {:?}", now);
}
If we run this program, we get:
$ cargo run
Compiling hello v0.1.0 (file:///Users/mbutcher/Code/Rust/hello)
Finished dev [unoptimized + debuginfo] target(s) in 1.62 secs
Running `target/debug/hello`
It is now SystemTime { tv_sec: 1527461839, tv_nsec: 389866000 }
Okay, now let's see what we've discovered from this example.
First, we want to work with the standard time library. In Rust parlance, we use use
to bring something into scope. Since we need to access the system time, we bring it into scope like this:
use std::time::SystemTime;
Technically speaking, we don't need to use use
in order to make a library available to us. (In other words, use
lines aren't relied upon by the linker.) So we could omit the use
line, and call std::time::SystemTime::new()
in our code. Or we could use std::time
and then call time::SystemTime::new()
. But the most common practice is to import the thing or things you are using to make your code as easy to read as possible.
SystemTime
is a struct. And SystemTime::now()
constructs a new system time with its content set to the current system's time. Unlike Go, but like most languages, Rust represents time as elapsed time since UNIX epoch.
The next useful thing we see in this example is how to declare a variable: let now = //...
. In Rust, variables are immutable by default. Which means if we tried increment now
by two seconds, it would fail:
use std::time::{SystemTime, Duration};
fn main() {
let now = SystemTime::now();
now = now + Duration::from_secs(2);
println!("It is now {:?}", now);
}
Doing a cargo run
will result in an error saying something like cannot assign twice to immutable variable 'now'
.
Idiomatic naming: Modules are
lowercase
. Structs areCamelCase
. Variables, methods and functions aresnake_case
.
To change a variable from immutable to mutable, we add mut
between let
and the assignment operator:
use std::time::{SystemTime, Duration};
fn main() {
let mut now = SystemTime::now();
now = now + Duration::from_secs(2);
println!("It is now {:?}", now);
}
There are two other quick things to glean for this example, then we'll go on to the printing part.
- We can see how to handle durations in Rust (Using
Duration::from_*
methods) - We can see that the
+
operator can be used on times, which are not primitive types. This is becauseSystemTime::add
implements theAdd<Duration>
trait (like this). From that, Rust can determine how to applySystemTime + Duration
. This is a very useful feature that Go does not have.
Traits are like supercharged Go interfaces. Implementing a trait in Rust produces approximately the same effect as implementing an interface in Go. In Rust, though, implementing a trait must be done explicitly (
impl MyTrait for MyType {}
).
Alright, we're down to the last interesting line of our time printer:
println!("It is now {:?}", now);
As noted before, println!
is a macro. It fills the same role as Go's fmt.Println
, except it also allows formatting strings (sorta like an imaginary fmt.Printlnf
function).
Similar functions are all implemented as macros:
print!
is equivalent tofmt.Printf
eprint!
is equivalent tofmt.Fprintf(os.Stderr, ...)
format!
is equivalent tofmt.Sprintf
Formatting in Rust is quite a bit more sophisticated than the Go fmt
package. But the basics are fairly easy:
{}
will print the thing in its "display mode" (e.g. with the intention of displaying it to users){:?}
will print the thing in its "debug mode".{2}
will print the third passed-in parameter.{last}
will print the parameter namedlast
Here's a quick example of all four used together:
println!("{} {:?} {2} {last}", "first", "second", "third", last="fourth");
This produces:
first "second" third fourth
In our code, we used println!("{:?}")
because the std::time::SystemTime
struct does not implement Display
, which means (in practice) that println!("{}")
doesn't work. println!("It is now {}", now);
results in the error std::time::SystemTime
doesn't implement std::fmt::Display
.
But it does implement Debug
, so we can use "{:?}"
and see a representation of the SystemTime
object:
It is now SystemTime { tv_sec: 1527473298, tv_nsec: 570487000 }
Using external libraries
Our goal, at this point, is to make a Good morning/Good evening world app. We just saw how to work with raw times, so now we can get about the business of making our little tool.
I'm going to admit something. Please don't be mad. I swear that (a) it's for your own good, and (b) I actually didn't realize when I started that we'd have to do this. But... here goes.... Unlike Go's time
package, the Rust std::time
package doesn't have a formatter. Actually, it's a little worse than that. The standard time library in Rust doesn't have a built in concept of time units other than seconds and nanoseconds.
So... we're gonna do a lot of math!
Just kidding. We're gonna use a library that provides a broader notion of time. It's called Chrono.
In the previous example, we used an internal standard library. Now we are going to use an external library. This means, on a practical level, we are going to have to do three things:
- Tell Cargo that we need it to get the library for us
- Tell the compiler/linker that we are using this external library (in
main.rs
) - Then
use
the library
Let's start with the first one. For that, we need to take a two-sentence detour. Rust uses a standard package management system in which packages (crates) can be referenced by name, and automatically tracked by version. In the Rust world, the idiomatic (it never gets old!) place to find packages is Crates.io.
I looked at various time modules on crates.io, and found the Chrono crate to be the right one for my date/time needs. (Exposé: All I really did was search for "time" and looked at download count.)
The first line of the Chrono page instructs me to put chrono = "0.4.2"
in my Cargo.toml
file. I'm good at following directions.
[package]
name = "hello"
version = "0.1.0"
authors = ["Matt Butcher <me@example.com>"]
[dependencies]
chrono = "0.4.2"
It doesn't say to do anything else. I'm good at not following non-existent instructions.
Next up, it's time to add that package to our code, and then do a small bit of retrofitting to make our last example work again:
extern crate chrono;
use chrono::prelude::*;
fn main() {
let now = Local::now();
println!("It is now {:?}", now);
}
Note that we've changed just a couple of things:
extern crate chrono
tells the compiler system that we're using an external create namedchrono
. (Go collapses the duties ofextern
anduse
intoimport
)use chrono::prelude::*
tells Rust to import all of the stuff in the Chrono module calledprelude
(aptly summarized as "the stuff you usually need"). At the moment we are just usingLocal
, so we could simplify this tochrono::prelude::Local
.Local::now()
is the Chrono constructor for creating a new localized time. (As opposed toUtc::now()
, which does not set a timezone).
Something interesting happens when we cargo run
this:
$ cargo run
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading chrono v0.4.2
Downloading num-integer v0.1.38
Downloading num-traits v0.2.4
Downloading time v0.1.40
Downloading libc v0.2.41
Compiling num-traits v0.2.4
Compiling num-integer v0.1.38
Compiling libc v0.2.41
Compiling time v0.1.40
Compiling chrono v0.4.2
Compiling hello v0.1.0 (file:///Users/mbutcher/Code/Rust/hello)
Finished dev [unoptimized + debuginfo] target(s) in 7.50 secs
Running `target/debug/hello`
It is now 2018-05-27T20:35:07.581600-06:00
Recall that we added chrono = "0.4.2"
to our Cargo.toml
, but didn't do anything else. At that point, we didn't actually have the library installed. It wasn't until running cargo run
that the system detected that we needed a library that was not present. It first grabbed a copy of the Crates.io index, located Chrono, and then installed it. Chrono also has a number of dependencies, so Cargo downloaded and installed those as well.
Then, finally, it compiled our program and ran it.
Next, instead of printing out the time in seconds and nanoseconds, our new version printed out an RFC8601 date:
It is now 2018-05-27T20:35:07.581600-06:00
This is the debug representation of a Chrono timestamp.
We're just about done with our program.
Matching Up
Our next pass is going to take an interesting divergence from Go. We have a localized date/time, and we want to determine whether it's AM (so we can say good morning, world
) or PM (so we can say good day, world
).
The Chrono Locale
object implements a Timelike
trait (again, think Go interface) which has a method called hour12()
. And hour12()
returns two values: a boolean for AM (false) or PM (true), and an unsigned 32-bit integer (in Rust, this type is named u32
) with a value between 1 and 12.
In Go, we'd handle the situation like this:
if am_pm, _ := hour12(); am_pm {
// It's PM
} else {
// It's AM
}
Idiomatic (weee!) Rust is slightly different. First, Rust's if
does not have a separate initializer. Rust follows the C-like if
format: if CONDITION {} else if CONDITION {} else {}
.
So we could handle the hour12
case like this:
extern crate chrono;
use chrono::prelude::*;
fn main() {
let now = Local::now();
let (is_pm, _) = now.hour12();
if is_pm {
println!("Good day, world!");
} else {
println!("Good morning, world!");
}
}
Here, we just do the assignment on one line and the conditional on the following lines. Note that to assign multiple return values, we create a tuple (is_pm, _)
, telling Rust to assign the boolean to is_pm
and to ignore the hour. This feels more-or-less comfortable to us Go programmers.
But now we have some nearly duplicate println!
calls. And Rust lets us do something Go doesn't: Assign conditionally.
extern crate chrono;
use chrono::prelude::*;
fn main() {
let now = Local::now();
let (is_pm, _) = now.hour12();
let am_pm = if is_pm {
"day"
} else {
"morning"
};
println!("Good {}, world!", am_pm);
}
By subtly changing the syntax of our conditional, we've used it for assignment. Note that "day"
and "morning"
do not have semicolons, but that the if/else block is terminated with };
. Basically, when a block is executed, it's last statement is returned. So when the if/else block is executed, either "day" or "morning" is returned to the let am_pm =
assignment.
This is cool and useful. But we can do it more compactly without if/else
:
extern crate chrono;
use chrono::prelude::*;
fn main() {
let now = Local::now();
let am_pm = match now.hour12() {
(false, _) => "morning",
(_, _) => "day",
};
println!("Good {}, world!", am_pm);
}
Now we're using a match
instead of an if/else
. Match is structurally similar to Go's case
statement. The match
phrase tells us what we're trying to match, and each entry inside the match
provides match criteria. But a match must cover all possible cases.
So a simple match might look like this:
let int = 3;
match int {
1 => {
println!("first")
},
2 => {
println!("second")
}
_ => {
println!("something else")
}
}
We can set int
to different values to see how this behaves. But essentially, it will match the value of int
to the first condition it satisfies. If let int = 1
, then the above will print first
. 2
will print second
. Any other number (matched by _
) will print something else
.
We use our match
to assign to am_pm
, and we are matching against a tuple. So we do this:
let am_pm = match now.hour12() {
(false, _) => "morning",
(_, _) => "day",
};
And what we mean is:
Assign "morning" to
am_pm
if the first valuehour12()
returns is false. In all other cases, assign "day" toam_pm
.
As I obsess about making this compact, there's one more way we could make this code even shorter. We can go back to our if
statement, but just index the tuple:
extern crate chrono;
use chrono::prelude::*;
fn main() {
let now = Local::now();
let am_pm = if now.hour12().0 { "day" } else { "morning" };
println!("Good {}, world!", am_pm);
}
The .0
part tells Rust to use the first (0th) item in the tuple that hour12()
returned.
So which is most idiomatic for Rust? I looked through all of the docs, including the style guide, and my conclusion is that nobody particularly cares which solution you choose. Just opt for readability. That's a nice feeling.
Conclusion
Go and Rust are often compared to each other, probably unfairly. In fact, they are very different languages, each created with separate goals. But what I've tried to do here is start from some similarities and introduce the basics of Rust by comparing it with Go.
I'd like to do follow-up posts covering things like testing, error handling, and working with types and data structures. But perhaps the above is sufficient to get you interested in reading the real Rust book.
DISCLAIMER: I'm a total Rust neophyte, and don't know all of the terminology or mechanics that well. My apologies.