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.
Join the DZone community and get the full member experience.
Join For Freeyou'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 it's 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 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 from this example, then we'll go on to the printing part.
-
we can see how to handle durations in rust (using the
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.
Published at DZone with permission of Matt Butcher, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments