Rust for Go developers
You know Go. This page lists what you can keep, ten habits that will meet a compiler error, and where each is taught. Both languages ran in one image (Go 1.27.3, Rust 1.99.1, Linux ARM64), and all output and compiler messages are what the runs printed.
RememberGo trusts you with sharing and changing, and leaves the rest to go vet, the race detector and tests. Rust asks who owns a value, who may read it and who may change it, and refuses the program when the answer is unclear.
What carries over
Section titled “What carries over”- Errors are values. A failing call returns its error and you decide.
Result<T, E>holds either the value or the error, where Go returns both (Errors and Result). - Structs and methods, no classes. Data in a struct, methods in an
implblock (Structs, Methods). - A slice is a pointer, a length and a capacity. A
Vechas the same three words (Ownership), and&v[1..3]is a window likev[1:3]. - Interfaces name what a function needs. A trait does that job, and also works as a generic bound (Traits).
- One tool does the chores.
cargobuilds, runs, tests and formats likego(toolchain table below).
Ten habits to unlearn
Section titled “Ten habits to unlearn”Each habit shows a Go program, the Rust that replaces it, and the compiler’s message where Rust refuses.
1. range hands you a copy
Section titled “1. range hands you a copy”In Go, p in for _, p := range people is a copy, so p.Age++ changes the copy (the first line below shows the ages unchanged); you fix it by indexing. In Rust the loop says what you get: &people lends each element read-only, &mut people lends it for changing, and plain people moves the elements out.
package main
import "fmt"
type Person struct { Name string Age int}
func main() { people := []Person{{"ana", 30}, {"bo", 41}} for _, p := range people { p.Age++ // p is a copy of the element } fmt.Println(people) for i := range people { people[i].Age++ } fmt.Println(people)}[{ana 30} {bo 41}]
[{ana 31} {bo 42}]struct Person { name: String, age: u32,}
fn main() { let mut people = vec![ Person { name: "ana".into(), age: 30, }, Person { name: "bo".into(), age: 41, }, ]; for p in &mut people { p.age += 1; // p is a &mut Person: it reaches the element itself } for p in &people { println!("{} {}", p.name, p.age); }}ana 31 bo 42
The loop a Go programmer writes first, for p in &people with p.age += 1, is refused: a & reference cannot write.
error[E0594]: cannot assign to `p.age`, which is behind a `&` reference --> src/main.rs:12:9 | 11 | for p in &people { | ------- this iterator yields `&` references 12 | p.age += 1; | ^^^^^^^^^^ `p` is a `&` reference, so it cannot be written to | help: use a mutable iterator instead | 11 | for p in &mut people { | +++
The whole failing program
struct Person { name: String, age: u32,}
fn main() { let people = vec![Person { name: "ana".into(), age: 30, }]; for p in &people { p.age += 1; } println!("{}", people[0].name);}Taught in Looping over collections and Copy or reference.
2. nil is a value you can forget to check
Section titled “2. nil is a value you can forget to check”Go compiled u.Name on a nil *User and crashed at run time. In Rust, references and Box cannot be null: “maybe no user” is the type Option<User>, and the field is out of reach until you match.
package main
import "fmt"
type User struct{ Name string }
func find(id int) *User { if id == 1 { return &User{"ana"} } return nil}
func main() { u := find(2) fmt.Println(u.Name)}panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xa1ff0] goroutine 1 [running]: main.main() /home/you/for-go/go/h02/main.go:16 +0x20 exit status 2
This output changes from run to run. This is one real run.
struct User { name: String,}
fn find(id: u32) -> Option<User> { if id == 1 { Some(User { name: "ana".into() }) } else { None }}
fn main() { match find(2) { Some(u) => println!("{}", u.name), None => println!("no such user"), }}no such user
Reading u.name straight off the Option is the same mistake; the build stops it:
error[E0609]: no field `name` on type `Option<User>` --> src/main.rs:15:22 | 15 | println!("{}", u.name); | ^^^^ unknown field | help: one of the expressions' fields has a field of the same name | 15 | println!("{}", u.unwrap().name); | +++++++++
The whole failing program
struct User { name: String,}
fn find(id: u32) -> Option<User> { if id == 1 { Some(User { name: "ana".into() }) } else { None }}
fn main() { let u = find(2); println!("{}", u.name);}unwrap() is there when you want a crash, but you write it (task 1 below). Taught in Option.
3. if err != nil becomes a type, and _ gets louder
Section titled “3. if err != nil becomes a type, and _ gets louder”n, _ := strconv.Atoi("abc") throws the error away: n is 0 and the program printed 1. In Rust, parse returns a Result<i32, ParseIntError>, which does not compile where an i32 is wanted. ? is close to a one-character if err != nil { return err }, and it converts the error type if needed.
package main
import ( "fmt" "strconv")
func main() { n, _ := strconv.Atoi("abc") fmt.Println(n + 1)
m, err := strconv.Atoi("abc") fmt.Println(m, err)}1 0 strconv.Atoi: parsing "abc": invalid syntax
use std::num::ParseIntError;
fn double(s: &str) -> Result<i32, ParseIntError> { let n: i32 = s.parse()?; Ok(n * 2)}
fn main() { println!("{:?}", double("21")); println!("{:?}", double("abc"));}Ok(42)
Err(ParseIntError { kind: InvalidDigit })error[E0308]: mismatched types --> src/main.rs:2:18 | 2 | let n: i32 = "abc".parse(); | --- ^^^^^^^^^^^^^ expected `i32`, found `Result<_, _>` | | | expected due to this | = note: expected type `i32` found enum `Result<_, _>` help: consider using `Result::expect` to unwrap the `Result<_, _>` value, panicking if the value is a `Result::Err` | 2 | let n: i32 = "abc".parse().expect("REASON"); | +++++++++++++++++
The whole failing program
fn main() { let n: i32 = "abc".parse(); println!("{}", n + 1);}Taught in Errors and Result, which also shows the warning for ignoring a Result.
4. A type fits an interface by having the methods
Section titled “4. A type fits an interface by having the methods”In Go, Robot has a Speak method, so it is a Speaker. In Rust only impl Speaker for Robot makes it one; a method with the same name outside that block does not count.
package main
import "fmt"
type Speaker interface{ Speak() string }
type Robot struct{}
func (Robot) Speak() string { return "beep" }
func introduce(s Speaker) { fmt.Println(s.Speak()) }
func main() { introduce(Robot{}) // Robot has Speak, so it is a Speaker}beep
trait Speaker { fn speak(&self) -> String;}
struct Robot;
impl Speaker for Robot { fn speak(&self) -> String { "beep".to_string() }}
fn introduce(s: &impl Speaker) { println!("{}", s.speak());}
fn main() { introduce(&Robot);}beep
error[E0277]: the trait bound `Robot: Speaker` is not satisfied --> src/main.rs:18:15 | 18 | introduce(&Robot); | --------- ^^^^^^ unsatisfied trait bound | | | required by a bound introduced by this call | help: the trait `Speaker` is not implemented for `Robot` --> src/main.rs:5:1 | 5 | struct Robot; | ^^^^^^^^^^^^ help: this trait has no implementations, consider adding one --> src/main.rs:1:1 | 1 | trait Speaker { | ^^^^^^^^^^^^^ note: required by a bound in `introduce` --> src/main.rs:13:23 | 13 | fn introduce(s: &impl Speaker) { | ^^^^^^^ required by this bound in `introduce`
The whole failing program
trait Speaker { fn speak(&self) -> String;}
struct Robot;
impl Robot { fn speak(&self) -> String { "beep".to_string() }}
fn introduce(s: &impl Speaker) { println!("{}", s.speak());}
fn main() { introduce(&Robot);}The impl costs a line. In return you can implement your own trait for a type you did not write, i32 included, which Go does not allow. Rust refuses someone else’s trait on someone else’s type (the orphan rule). Both are in Traits.
5. Slices share an array; a Vec owns its items
Section titled “5. Slices share an array; a Vec owns its items”In Go, b := a[:2] is a window onto a’s array, and append(b, 99) found room there and overwrote a[2]. In Rust a slice borrows from the Vec, which cannot grow while the borrow is alive. For an independent list you write to_vec().
package main
import "fmt"
func main() { a := []int{1, 2, 3, 4} b := a[:2] b = append(b, 99) // there is room in a's array fmt.Println(a, b)}[1 2 99 4] [1 2 99]
fn main() { let a = vec![1, 2, 3, 4]; let mut b = a[..2].to_vec(); // an explicit copy of two items b.push(99); println!("{a:?} {b:?}");}[1, 2, 3, 4] [1, 2, 99]
error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable --> src/main.rs:4:5 | 3 | let b = &a[..2]; | - immutable borrow occurs here 4 | a.push(99); | ^^^^^^^^^^ mutable borrow occurs here 5 | println!("{b:?}"); | - immutable borrow later used here
The whole failing program
fn main() { let mut a = vec![1, 2, 3, 4]; let b = &a[..2]; a.push(99); println!("{b:?}");}Taught in Vectors and Borrowing.
6. Fields have no automatic zero value
Section titled “6. Fields have no automatic zero value”User{Name: "ana"} in Go fills the other fields with zeros, and Go code often relies on that zero value. In Rust, leaving a field out does not build. ..Default::default() asks for the zeros, for a type that implements Default.
package main
import "fmt"
type User struct { Name string Age int Active bool}
func main() { u := User{Name: "ana"} fmt.Printf("%+v\n", u)}{Name:ana Age:0 Active:false}#[derive(Default)]struct User { name: String, age: u32, active: bool,}
fn main() { let u = User { name: "ana".into(), ..Default::default() }; println!("{} {} {}", u.name, u.age, u.active);}ana 0 false
error[E0063]: missing fields `active` and `age` in initializer of `User` --> src/main.rs:8:13 | 8 | let u = User { name: "ana".into() }; | ^^^^ missing `active` and `age`
The whole failing program
struct User { name: String, age: u32, active: bool,}
fn main() { let u = User { name: "ana".into() }; println!("{} {} {}", u.name, u.age, u.active);}For an absent field, Go reaches for a pointer and Rust for Option (habit 2). Taught in Structs.
7. Embedding does not exist
Section titled “7. Embedding does not exist”Go promoted Name and Describe from Animal to Dog. In Rust Dog holds an Animal in a named field, and you reach through it or write a forwarding method. Shared behaviour goes in a trait, optionally with default methods (Traits).
package main
import "fmt"
type Animal struct{ Name string }
func (a Animal) Describe() string { return a.Name + " is an animal" }
type Dog struct { Animal Tricks int}
func main() { d := Dog{Animal{"rex"}, 2} fmt.Println(d.Name, "|", d.Describe())}rex | rex is an animal
struct Animal { name: String,}
impl Animal { fn describe(&self) -> String { format!("{} is an animal", self.name) }}
struct Dog { animal: Animal, #[allow(dead_code)] tricks: u32,}
impl Dog { fn describe(&self) -> String { self.animal.describe() // forward by hand }}
fn main() { let d = Dog { animal: Animal { name: "rex".into() }, tricks: 2, }; println!("{} | {}", d.animal.name, d.describe());}rex | rex is an animal
error[E0609]: no field `name` on type `Dog` --> src/main.rs:19:27 | 19 | println!("{} | {}", d.name, d.describe()); | ^^^^ unknown field | help: one of the expressions' fields has a field of the same name | 19 | println!("{} | {}", d.animal.name, d.describe()); | +++++++ error[E0599]: no method named `describe` found for struct `Dog` in the current scope --> src/main.rs:19:35 | 11 | struct Dog { | ---------- method `describe` not found for this struct ... 19 | println!("{} | {}", d.name, d.describe()); | ^^^^^^^^ method not found in `Dog` | help: one of the expressions' fields has a method of the same name | 19 | println!("{} | {}", d.name, d.animal.describe()); | +++++++ Some errors have detailed explanations: E0599, E0609.
The whole failing program
struct Animal { name: String,}
impl Animal { fn describe(&self) -> String { format!("{} is an animal", self.name) }}
struct Dog { animal: Animal,}
fn main() { let d = Dog { animal: Animal { name: "rex".into() }, }; println!("{} | {}", d.name, d.describe());}Forwarding is more typing, and that is the cost. Both messages name the field.
8. defer runs at the end of the function; Drop runs at the end of the scope
Section titled “8. defer runs at the end of the function; Drop runs at the end of the scope”Go ran the three deferred prints after the body of main, last one first. A Rust value with a Drop impl is cleaned up when its scope ends, here one loop iteration, and the cleanup belongs to the type.
package main
import "fmt"
func main() { for i := 1; i <= 3; i++ { defer fmt.Println("deferred", i) fmt.Println("loop", i) } fmt.Println("end of main")}loop 1 loop 2 loop 3 end of main deferred 3 deferred 2 deferred 1
struct Guard(u32);
impl Drop for Guard { fn drop(&mut self) { println!("dropped {}", self.0); }}
fn main() { for i in 1..=3 { let _g = Guard(i); println!("loop {i}"); } println!("end of main");}loop 1 dropped 1 loop 2 dropped 2 loop 3 dropped 3 end of main
Taught in Common traits. The Rust Reference defines drop scopes; the Go spec defines defer as running when the surrounding function returns. Task 2 drops a value at once.
9. Goroutines can share whatever they can reach
Section titled “9. Goroutines can share whatever they can reach”Two goroutines added to one count. Go compiled it, and count++ is a read, an add and a write, so updates were lost: the first step below printed a total under 2,000,000 (it changes from run to run), and the second shows the race detector’s first line. In Rust, two closures that both change count do not build; the fix is to put the data inside a lock, which is a type.
package main
import ( "fmt" "sync")
func main() { var wg sync.WaitGroup count := 0 for range 2 { wg.Add(1) go func() { defer wg.Done() for range 1_000_000 { count++ } }() } wg.Wait() fmt.Println(count)}1259304$ go run -race ./h09 2>&1 | grep -m1 "WARNING: DATA RACE"
WARNING: DATA RACE
This output changes from run to run. This is one real run.
use std::sync::Mutex;use std::thread;
fn main() { let count = Mutex::new(0); thread::scope(|s| { for _ in 0..2 { s.spawn(|| { for _ in 0..1_000_000 { *count.lock().unwrap() += 1; } }); } }); println!("{}", count.into_inner().unwrap());}2000000
error[E0499]: cannot borrow `count` as mutable more than once at a time --> src/main.rs:7:21 | 5 | thread::scope(|s| { | - has type `&'1 Scope<'1, '_>` 6 | for _ in 0..2 { 7 | s.spawn(|| { | - ^^ `count` was mutably borrowed here in the previous iteration of the loop | _____________| | | 8 | | for _ in 0..1_000_000 { 9 | | count += 1; | | ----- borrows occur due to use of `count` in closure 10 | | } 11 | | }); | |______________- argument requires that `count` is borrowed for `'1` ... (notes pointing into the standard library trimmed)
The whole failing program
use std::thread;
fn main() { let mut count = 0; thread::scope(|s| { for _ in 0..2 { s.spawn(|| { for _ in 0..1_000_000 { count += 1; } }); } }); println!("{count}");}thread::spawn starts an operating-system thread, where go hands a function to Go’s scheduler (Threads counts both). Async code needs a runtime you choose. Taught in Shared state, Send and Sync and Async basics.
10. Go has one string; Rust has two, and neither is indexed by byte
Section titled “10. Go has one string; Rust has two, and neither is indexed by byte”A Go string is bytes: len(s) counts them, s[1] is one of them, and s[:2] cut the é in half without a word ("h\xc3" is not valid UTF-8). Rust has String (owns the text) and &str (borrows it). s[1] does not compile. &s[..2] does compile, and panics at run time when 2 falls inside a character; get is the form that returns an Option, so that cut is None.
package main
import "fmt"
func main() { s := "héllo" fmt.Println(len(s), len([]rune(s)), s[1]) fmt.Printf("%q\n", s[:2])}6 5 195 "h\xc3"
fn main() { let s = "héllo"; println!("{} {}", s.len(), s.chars().count()); println!("{:?}", s.get(..2)); println!("{:?}", s.get(..3));}6 5
None
Some("hé")error[E0277]: the type `str` cannot be indexed by `{integer}` --> src/main.rs:3:18 | 3 | let byte = s[1]; | ^ string indices are ranges of `usize` | = help: the trait `SliceIndex<str>` is not implemented for `{integer}` = note: you can use `.chars().nth()` or `.bytes().nth()` for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings> ... (notes pointing into the standard library trimmed)
The slicing form a Go programmer types next builds, and stops the program:
thread 'main' (2478) panicked at src/bin/h10slice.rs:3:22:
end byte index 2 is not a char boundary; it is inside 'é' (bytes 1..3 of string)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceThe whole failing program
fn main() { let s = "héllo"; let byte = s[1]; println!("{byte}");}A function that only reads text takes &str. Taught in Strings.
Smaller changes: a capital letter exports a Go name and pub does it in Rust (Modules), and a struct tag like json:"name" becomes an attribute (Serde).
Idiom by idiom
Section titled “Idiom by idiom”The two programs behind this table print the same 20 lines, which the last run below checks with diff.
| You write in Go | In Rust | Read |
|---|---|---|
x := 5 then x = x + 1 |
let mut x = 5; then x += 1; |
Variables |
var zero int |
let zero = i32::default(); |
Common traits |
for i := 0; i < 3; i++ { sum += i } |
for i in 0..3 { sum += i; } |
Loops |
for i, w := range words { total += i * len(w) } |
for (i, w) in words.iter().enumerate() { total += i * w.len(); } |
Looping over collections |
switch day { case "sat", "sun": kind = "weekend" } |
let kind = match day { "sat" | "sun" => "weekend", _ => "weekday" }; |
Match |
if v, ok := ages["ana"]; ok { ... } |
if let Some(v) = ages.get("ana") { ... } |
Maps |
counts["the"]++ |
*counts.entry("the").or_insert(0) += 1; |
Maps |
v = append(v, 2) |
v.push(2); |
Vectors |
len(s) (bytes) |
s.len() (bytes) |
Strings |
len([]rune(s)) |
s.chars().count() |
Strings |
len(strings.Fields("a b")) |
"a b".split_whitespace().count() |
Text |
n, err := strconv.Atoi("42") |
"42".parse::<i32>(), a Result |
Errors and Result |
fmt.Sprintf("%s-%d", a, b) |
format!("{a}-{b}") |
Formatting |
sort.Ints(v) |
v.sort(); |
Vectors |
strings.ToUpper("ana") (ASCII here) |
"ana".to_uppercase() |
Text |
a, b = b, a |
(a, b) = (b, a); |
Tuples |
min(p, q) |
p.min(q) |
Common traits |
type Celsius float64 |
struct Celsius(f64);, read as c.0; no arithmetic until you add it |
Structs |
errors.New("bad input") |
let e: Box<dyn Error> = "bad input".into(); |
Errors and Result |
func (c *Counter) Inc() { c.n++ } |
fn inc(&mut self) { self.n += 1; } |
Methods |
The two programs and their runs
package main
import ( "errors" "fmt" "sort" "strconv" "strings")
type Celsius float64
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func main() { x := 5 x = x + 1 fmt.Println("reassign:", x)
var zero int fmt.Println("zero:", zero)
sum := 0 for i := 0; i < 3; i++ { sum += i } fmt.Println("range:", sum)
words := []string{"ana", "bo", "cy"} total := 0 for i, w := range words { total += i * len(w) } fmt.Println("weighted:", total)
day := "sat" kind := "weekday" switch day { case "sat", "sun": kind = "weekend" } fmt.Println("kind:", kind)
ages := map[string]int{"ana": 30} if v, ok := ages["ana"]; ok { fmt.Println("lookup: found", v) }
counts := map[string]int{} counts["the"]++ counts["the"]++ fmt.Println("count:", counts["the"])
v := []int{3, 1} v = append(v, 2) fmt.Println("len:", len(v))
s := "héllo" fmt.Println("bytes:", len(s)) fmt.Println("chars:", len([]rune(s))) fmt.Println("fields:", len(strings.Fields("a b")))
if n, err := strconv.Atoi("42"); err == nil { fmt.Println("parsed: ok", n) }
fmt.Println("formatted:", fmt.Sprintf("%s-%d", "ana", 7))
sort.Ints(v) fmt.Println("smallest:", v[0])
fmt.Println("upper:", strings.ToUpper("ana"))
a, b := 1, 2 a, b = b, a fmt.Println("swapped:", a, b)
p, q := 3, 5 fmt.Println("min:", min(p, q))
c := Celsius(21.5) fmt.Println("newtype:", float64(c))
e := errors.New("bad input") fmt.Println("error:", e)
var ctr Counter ctr.Inc() fmt.Println("method:", ctr.n)}reassign: 6 zero: 0 range: 3 weighted: 6 kind: weekend lookup: found 30 count: 2 len: 3 bytes: 6 chars: 5 fields: 2 parsed: ok 42 formatted: ana-7 smallest: 1 upper: ANA swapped: 2 1 min: 3 newtype: 21.5 error: bad input method: 1
use std::collections::HashMap;use std::error::Error;
struct Celsius(f64);
struct Counter { n: u32,}
impl Counter { fn inc(&mut self) { self.n += 1; }}
fn main() { let mut x = 5; x += 1; println!("reassign: {x}");
let zero = i32::default(); println!("zero: {zero}");
let mut sum = 0; for i in 0..3 { sum += i; } println!("range: {sum}");
let words = ["ana", "bo", "cy"]; let mut total = 0; for (i, w) in words.iter().enumerate() { total += i * w.len(); } println!("weighted: {total}");
let day = "sat"; let kind = match day { "sat" | "sun" => "weekend", _ => "weekday", }; println!("kind: {kind}");
let ages = HashMap::from([("ana", 30)]); if let Some(v) = ages.get("ana") { println!("lookup: found {v}"); }
let mut counts: HashMap<&str, u32> = HashMap::new(); *counts.entry("the").or_insert(0) += 1; *counts.entry("the").or_insert(0) += 1; println!("count: {}", counts["the"]);
let mut v = vec![3, 1]; v.push(2); println!("len: {}", v.len());
let s = "héllo"; println!("bytes: {}", s.len()); println!("chars: {}", s.chars().count()); println!("fields: {}", "a b".split_whitespace().count());
if let Ok(n) = "42".parse::<i32>() { println!("parsed: ok {n}"); }
let (a, b) = ("ana", 7); let text = format!("{a}-{b}"); println!("formatted: {text}");
v.sort(); println!("smallest: {}", v[0]);
println!("upper: {}", "ana".to_uppercase());
let (mut a, mut b) = (1, 2); (a, b) = (b, a); println!("swapped: {a} {b}");
let (p, q) = (3, 5); println!("min: {}", p.min(q));
let c = Celsius(21.5); println!("newtype: {}", c.0);
let e: Box<dyn Error> = "bad input".into(); println!("error: {e}");
let mut ctr = Counter { n: 0 }; ctr.inc(); println!("method: {}", ctr.n);}reassign: 6 zero: 0 range: 3 weighted: 6 kind: weekend lookup: found 30 count: 2 len: 3 bytes: 6 chars: 5 fields: 2 parsed: ok 42 formatted: ana-7 smallest: 1 upper: ANA swapped: 2 1 min: 3 newtype: 21.5 error: bad input method: 1
$ go run ./idioms > go.txt; cargo run --bin idioms > rust.txt; diff go.txt rust.txt; wc -l < go.txt
the two outputs are identical
20
The toolchain, side by side
Section titled “The toolchain, side by side”| Job | Go | Rust |
|---|---|---|
| New project | go mod init example.com/demo |
cargo new demo |
| Add a dependency | go get (not run) |
cargo add (needs the network; not run, see Finding and using crates) |
| Build | go build ./... |
cargo build |
| Run | go run . |
cargo run |
| Test | go test ./... |
cargo test, which also runs the examples in doc comments |
| Format | gofmt -l . |
cargo fmt --check |
| Lint | go vet ./... |
cargo clippy |
| Docs | go doc strings.Fields |
cargo doc --no-deps, which writes HTML |
| Debugger | Delve, dlv (not in the image) |
rust-gdb or rust-lldb from the Rust install, or gdb or lldb directly (not run) |
| Other platforms | GOOS=windows go build |
cargo build --target ... (see below) |
What the commands printed
$ cargo new demo --vcs none Creating binary (application) `demo` package note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html $ go mod init example.com/demo go: creating new go.mod: module example.com/demo $ for t in dlv rust-gdb rust-lldb gdb lldb; do command -v $t || echo "$t: not found"; done dlv: not found /opt/cargo/bin/rust-gdb /opt/cargo/bin/rust-lldb gdb: not found lldb: not found
$ go build ./... $ go test ./... ok calc 0.001s $ gofmt -l . $ go vet ./... $ go doc strings.Fields package strings // import "strings" func Fields(s string) []string Fields splits the string s around each instance of one or more consecutive white space characters, as defined by unicode.IsSpace, returning a slice of substrings of s or an empty slice if s contains only white space. Every element of the returned slice is non-empty. Unlike Split, leading and trailing runs of white space characters are discarded. $ go doc calc.Add package calc // import "calc" func Add(a, b int) int Add returns a + b.
This output changes from run to run. This is one real run.
$ cargo build $ cargo test Compiling calc v0.1.0 (/home/you/for-go/rust) Finished `test` profile [unoptimized + debuginfo] target(s) in 0.09s Running unittests src/lib.rs (target/debug/deps/calc-0d704d460dc14554) running 1 test test tests::adds ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests calc running 1 test test src/lib.rs - add (line 5) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s all doctests ran in 0.13s; merged doctests compilation took 0.13s $ cargo fmt --check $ cargo clippy $ cargo doc --no-deps Documenting calc v0.1.0 (/home/you/for-go/rust) Generated target/doc/calc/index.html
This output changes from run to run. This is one real run.
gofmt -l, go vet and cargo fmt --check print nothing when they find nothing. dlv, gdb and lldb were missing in the image, so no debugger ran. See Your first project for the Rust commands and Testing for more on cargo test.
A reading path
Section titled “A reading path”- Rust at a glance: a Go table of the syntax.
- What Rust lacks: what you give up (
nil,defer,goto) and what replaces it. - Copy or reference: range copies, slice headers, closures.
- Ownership: the move, which Go has no word for, and the type, not escape analysis, decides stack or heap.
- Borrowing and Mutable borrows: read and write, split apart.
- Option and Errors and Result: for
nilandif err != nil. - Enums: a closed list, which a type switch is not.
- Traits and Trait objects: interfaces you declare, and the
dynform. - Vectors and Strings: slice and string habits.
- Returning references and Lifetimes: why returning a pointer to a local is fine in Go and refused here.
- Working with the borrow checker: fixes for when the compiler says no.
- Threads, Shared state, Channels, Send and Sync: goroutines.
- Async basics, then Async with Tokio: Go has no async/await; goroutines and channels do that job.
- Modules and Idioms and design patterns: packages versus modules, then common shapes.
What you will miss
Section titled “What you will miss”Each point is backed by a run, a source or a lesson:
- Cross-compiling with two variables. Go built a Windows executable here with
GOOS=windows. The image’s Rust had one target, and asking for Windows failed. The rustup book saysrustup target addinstalls the standard library for a target and that you usually need a linker too. These steps need the network and were not run. - JSON in the standard library. Go marshals a struct with
encoding/json(below). Rust’sstdhas no JSON; the usual answer isserde(Serde, A map of crates). - Pointers that go anywhere. Go keeps what is reachable alive. Rust’s answers for graphs and parent links (indices,
Rc,Weak) are in Working with the borrow checker and Smart pointers. select. Channels instdhave none (Channels).- Joining two third-party pieces. In Go a type can satisfy an interface neither of you wrote. Rust’s orphan rule (habit 4) refuses that impl; a newtype is the way round (Traits).
$ GOOS=windows GOARCH=amd64 go build -o demo.exe ./h04 && ls demo.exe
demo.exe
$ rustup target list --installed aarch64-unknown-linux-gnu $ cargo build --target x86_64-pc-windows-gnu --bin h04 error[E0463]: can't find crate for `std` | = note: the `x86_64-pc-windows-gnu` target may not be installed = help: consider downloading the target with `rustup target add x86_64-pc-windows-gnu`
package main
import ( "encoding/json" "fmt")
type Item struct { Name string `json:"name"` Qty int `json:"qty"`}
func main() { out, err := json.Marshal(Item{"apple", 3}) fmt.Println(string(out), err)}{"name":"apple","qty":3} <nil>This page says nothing about compile times: they were not measured.
Try it yourself
Section titled “Try it yourself”Each task edits a program above; the sessions below ran in the image.
- In habit 2, replace the
matchwithfind(2).unwrap(). It builds, then panics withcalled `Option::unwrap()` on a `None` value: Go’s nil crash again, but you wrote the word that allows it. - In habit 8, change
let _g = Guard(i);tolet _ = Guard(i);. Eachdroppedline now comes before itsloopline:_does not bind the value, so nothing keeps it alive (Patterns). - In the failing program of habit 9, put
moveon the closures and keep the plaincount. It builds with two warnings and prints 0: each closure got its own copy of ani32. Safe Rust refuses data races; it does not refuse a program that gives the wrong answer.
thread 'main' (2830) panicked at src/main.rs:14:21:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace$ cargo run
dropped 1
loop 1
dropped 2
loop 2
dropped 3
loop 3
end of main
$ cargo run
warning: value captured by `count` is never read
--> src/main.rs:9:21
|
9 | count += 1;
| ^^^^^
|
= help: did you mean to capture by reference instead?
= note: `#[warn(unused_assignments)]` (part of `#[warn(unused)]`) on by default
warning: value assigned to `count` is never read
--> src/main.rs:9:21
|
9 | count += 1;
| ^^^^^^^^^^
|
= help: maybe it is overwritten before being read?
warning: `for-go` (bin "for-go") generated 2 warnings
0
Read more
Section titled “Read more”- The Rust Book, chapter 4: Understanding Ownership and chapter 9: Error Handling
- The Cargo Book: the
cargocommands in the toolchain table OptionandMutexin the std docs- Go: the Data Race Detector and
encoding/json, both used above - Glossary: the Rust words, with the closest Go idea for each
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.