Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Go Developer's Quickstart Guide to Rust

DZone's Guide to

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.

· Web Dev Zone ·
Free Resource

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

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.

Most Loved Languages

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 the import line
  • Change func to fn
  • Change fmt.Println to println!
  • 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 with cargo 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 are CamelCase. Variables, methods, and functions are snake_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 because SystemTime::add implements the Add<Duration> trait (like this). From that, Rust can determine how to apply SystemTime + 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 to fmt.Printf
  • eprint! is equivalent to fmt.Fprintf(os.Stderr, ...)
  • format! is equivalent to fmt.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 named last

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 named chrono (Go collapses the duties of extern and useinto import).
  • use chrono::prelude::* tells Rust to import all of the stuff in the Chrono module called prelude (aptly summarized as "the stuff you usually need"). At the moment we are just using Local, so we could simplify this to chrono::prelude::Local.
  • Local::now() is the Chrono constructor for creating a new localized time (as opposed to Utc::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 value hour12() returns is false. In all other cases, assign "day" to am_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.

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Topics:
rust language ,go language ,web dev ,rust

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}