I’ve been learning Rust for a few weeks now. It’s really interesting! I’m coming from Go so I’m used to a statically typed, compiled language and workflow.
This post is my opinion as a Rust beginner and Go seasoned developer. It’s probably full of blind spots, biases and errors. If you want to send your feedback, please do!
Types
I enjoy the richer type system, especially the enums: they allow expressing things with types that Go can only dream of ; and you get help from the compiler every step of the way.
Take this from Rust by Example:
// Create an `enum` to classify a web event. Note how both
// names and type information together specify the variant:
// `PageLoad != PageUnload` and `KeyPress(char) != Paste(String)`.
// Each is different and independent.
enum WebEvent {
// An `enum` variant may either be `unit-like`,
PageLoad,
PageUnload,
// like tuple structs,
KeyPress(char),
Paste(String),
// or c-like structures.
Click { x: i64, y: i64 },
}
// A function which takes a `WebEvent` enum as an argument and
// returns nothing.
fn inspect(event: WebEvent) {
match event {
WebEvent::PageLoad => println!("page loaded"),
WebEvent::PageUnload => println!("page unloaded"),
// Destructure `c` from inside the `enum` variant.
WebEvent::KeyPress(c) => println!("pressed '{}'.", c),
WebEvent::Paste(s) => println!("pasted \"{}\".", s),
// Destructure `Click` into `x` and `y`.
WebEvent::Click { x, y } => {
println!("clicked at x={}, y={}.", x, y);
},
}
}
fn main() {
let pressed = WebEvent::KeyPress('x');
// `to_owned()` creates an owned `String` from a string slice.
let pasted = WebEvent::Paste("my text".to_owned());
let click = WebEvent::Click { x: 20, y: 80 };
let load = WebEvent::PageLoad;
let unload = WebEvent::PageUnload;
inspect(pressed);
inspect(pasted);
inspect(click);
inspect(load);
inspect(unload);
}
In many other languages, enums have a single “backing type” (often a number). In Rust, enum values are not only a string or a number, they’re proper members of a type and can hold data. The compiler enforces that the match keyword covers all possible values, which means you can’t forget to handle a new enum value when you add one later.
How do I skin the cat?
I feel like there’s often multiple ways to do something. Say you want to iterate over items in a list, you can use an imperative loop or a functional approach. Want to make a String from a slice? You have at least 3 ways of doing so: s.to_string(), String::from(s) and s.to_owned().
I don’t mind the functional paradigm, but I dislike the fact that I have to wonder which approach is more idiomatic, appropriate, faster, consumes less memory…
In Go there’s usually one way to do things.
Want some libc with that compiled binary?
Some parts of Rust’s stlib depends on libc. That means more binaries to compile and a libc implementation available on the target system. It’s not an issue for learning, but it may become one later, I don’t know.
The Rust compiler is a librarian
Rust’s borrow checker is a blessing.
At first I didn’t understand what hit me. I just wanted to pass a parameter to a function.
Then the ownership model started making more and more sense. I believe it would take a few weeks or maybe a month of daily professional practice to get that engraved in my head, but it’s a really powerful model and seems obvious to a point where I was wondering why other languages do things differently after all.
This and other compiler features (macros 👀) lead to longer compilation times than Go.
crates.io
Crates are fine, but require a GitHub account to publish. And they require a separate publish step while Go is all about git push.
Also there are crates for things I believe should be available in the standard library (e.g. http client/server, de/serialization, async, …).
Imports and project structure
I still haven’t wrapped my head around a project’s directory and modules structure.
In Go:
A directory is a package, and the directory name is that package’s name.
The
packagedirective at the top of a Go file matches the directory name the file is in.To import a package:
import "github.com/someuser/modname"(there is no central registry in Go).To call a func from an imported package:
modname.Func(), refer to a struct:modname.Struct.
I believe this to be pretty straightforward.
In Rust, modules must be declared in a file higher in the hierarchy to exist. So in src/lib.rs if you see mod modname, it doesn’t mean that lib.rs is part of modname. On the contrary, it declares that another module called modname exists and the compiler will look in several well-known places to find it - or it could be defined just after the mod directive, in curly brackets.
A module can be declared public or not. Declaring a module brings it into scope. Also, modules can re-export things from inner modules.
Then there’s the use keyword, which look like an import, but not really, but sort of. You can sometimes use it only as a shortcut to save typing long paths, but you sometimes really need it to bring something into scope.
Finally, there are magic imports the compiler adds for you which is called the prelude.
I feel like that this whole situation is more confusing that it needs to be. And I haven’t even talked about crates yet.
No null
The banger: Rust has no concept of null or nil. It uses enums instead to convey the intention, and the compiler forces you to check for the presence or absence of value instead of maybe letting you running into a nil pointer panic at 3AM on a Sunday.
Traits vs interfaces
I have only scratched the surface of Rust’s traits but they seem promising. Go’s interfaces are implicitly satisfied: if a struct has a method DoStuff(), it satisfies the interface which requires the DoStuff() method without the interface or the struct having to know about each other.
In Rust, a trait has to be explicitly implemented on a struct for it to satisfy the trait, but you can implement a trait for a struct if you own at least one or the other. That means you can’t implement an stdlib trait on an stdlib struct, but you can implement an stdlib trait on one of your structs or one of your traits on an stdlib struct. This is powerful if a bit verbose, but at least there’s no need to resort to hacks to make sure the type always satisfies the expected interfaces as we may have to do in Go.
Cargo
Rust’s tooling is fine, I’d say it’s equivalent to Go. Benchmarks are lacking from stable Rust, parametrized tests are done using macros instead of iterating over a slice. Clippy is more strict than go vet (which is a good thing).
Learning material
The Rust Programming Language (“the Book”) is an excellent resource to learn Rust, especially the more visual and interactive edition. It takes small steps from complete beginner to building a multi-threaded web server.
Rust by Example takes another approach with less reading material and loads of examples as the title suggests.
There’s also rustlings which contain lots of small exercises to help you apply what you read in the Book. I recommend starting early otherwise you’ll need to catch up with many exercises that will probably be less interesting to you if you’re too far away.
I also recommend No Boilerplate’s YouTube playlist, not so much to learn but to give you the desire to learn.
All in all
I like learning new things, and with Rust I’m served!
Rust is a language where the compiler does a lot of upfront work for you, saving you down the road from unsavory bugs or other undefined behaviors. However, you need to tell the compiler what to do precisely, leading to some more syntax to learn and understand.
The language has a history and maybe went public too soon leading to the need to support old syntax, having “editions”, and a modules system that could be clearer.
I’d like something that combines Go’s expressiveness, “one way to do things” and batteries included stdlib with Rust’s no nil, enums and borrow checker ; as well as good out of the box tooling.
Further reading
Rust vs Go (Bitfield consulting)
Migrating from Go to Rust (corrode.dev)
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.