Skip to content
Learn Rust
Theme

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.

  • 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 impl block (Structs, Methods).
  • A slice is a pointer, a length and a capacity. A Vec has the same three words (Ownership), and &v[1..3] is a window like v[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. cargo builds, runs, tests and formats like go (toolchain table below).

Each habit shows a Go program, the Rust that replaces it, and the compiler’s message where Rust refuses.

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.

Gogo/h01/main.go
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)
}
$ go run ./h01
[{ana 30} {bo 41}]
[{ana 31} {bo 42}]
Rustrust/src/bin/h01.rs
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);
}
}
$ cargo run --bin h01
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.

$ cargo run
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
Rusth01.rs
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.

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.

Gogo/h02/main.go
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)
}
$ go run ./h02
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.

Rustrust/src/bin/h02.rs
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"),
}
}
$ cargo run --bin h02
no such user

Reading u.name straight off the Option is the same mistake; the build stops it:

$ cargo run
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
Rusth02.rs
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.

Gogo/h03/main.go
package main
import (
"fmt"
"strconv"
)
func main() {
n, _ := strconv.Atoi("abc")
fmt.Println(n + 1)
m, err := strconv.Atoi("abc")
fmt.Println(m, err)
}
$ go run ./h03
1
0 strconv.Atoi: parsing "abc": invalid syntax
Rustrust/src/bin/h03.rs
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"));
}
$ cargo run --bin h03
Ok(42)
Err(ParseIntError { kind: InvalidDigit })
$ cargo run
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
Rusth03.rs
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.

Gogo/h04/main.go
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
}
$ go run ./h04
beep
Rustrust/src/bin/h04.rs
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);
}
$ cargo run --bin h04
beep
$ cargo run
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
Rusth04.rs
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().

Gogo/h05/main.go
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)
}
$ go run ./h05
[1 2 99 4] [1 2 99]
Rustrust/src/bin/h05.rs
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:?}");
}
$ cargo run --bin h05
[1, 2, 3, 4] [1, 2, 99]
$ cargo run
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
Rusth05.rs
fn main() {
let mut a = vec![1, 2, 3, 4];
let b = &a[..2];
a.push(99);
println!("{b:?}");
}

Taught in Vectors and Borrowing.

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.

Gogo/h06/main.go
package main
import "fmt"
type User struct {
Name string
Age int
Active bool
}
func main() {
u := User{Name: "ana"}
fmt.Printf("%+v\n", u)
}
$ go run ./h06
{Name:ana Age:0 Active:false}
Rustrust/src/bin/h06.rs
#[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);
}
$ cargo run --bin h06
ana 0 false
$ cargo run
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
Rusth06.rs
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.

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).

Gogo/h07/main.go
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())
}
$ go run ./h07
rex | rex is an animal
Rustrust/src/bin/h07.rs
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());
}
$ cargo run --bin h07
rex | rex is an animal
$ cargo run
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
Rusth07.rs
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.

Gogo/h08/main.go
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")
}
$ go run ./h08
loop 1
loop 2
loop 3
end of main
deferred 3
deferred 2
deferred 1
Rustrust/src/bin/h08.rs
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");
}
$ cargo run --bin h08
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.

Gogo/h09/main.go
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)
}
$ go run ./h09
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.

Rustrust/src/bin/h09.rs
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());
}
$ cargo run --bin h09
2000000
$ cargo run
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
Rusth09.rs
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.

Gogo/h10/main.go
package main
import "fmt"
func main() {
s := "héllo"
fmt.Println(len(s), len([]rune(s)), s[1])
fmt.Printf("%q\n", s[:2])
}
$ go run ./h10
6 5 195
"h\xc3"
Rustrust/src/bin/h10.rs
fn main() {
let s = "héllo";
println!("{} {}", s.len(), s.chars().count());
println!("{:?}", s.get(..2));
println!("{:?}", s.get(..3));
}
$ cargo run --bin h10
6 5
None
Some("hé")
$ cargo run
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:

$ cargo run --bin h10slice
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 backtrace
The whole failing program
Rusth10.rs
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).

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
Gogo/idioms/main.go
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)
}
$ go run ./idioms
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
Rustrust/src/bin/idioms.rs
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);
}
$ cargo run --bin idioms
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
terminal
$ 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
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
terminal
$ 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
terminal
$ 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.

terminal
$ 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.

  1. Rust at a glance: a Go table of the syntax.
  2. What Rust lacks: what you give up (nil, defer, goto) and what replaces it.
  3. Copy or reference: range copies, slice headers, closures.
  4. Ownership: the move, which Go has no word for, and the type, not escape analysis, decides stack or heap.
  5. Borrowing and Mutable borrows: read and write, split apart.
  6. Option and Errors and Result: for nil and if err != nil.
  7. Enums: a closed list, which a type switch is not.
  8. Traits and Trait objects: interfaces you declare, and the dyn form.
  9. Vectors and Strings: slice and string habits.
  10. Returning references and Lifetimes: why returning a pointer to a local is fine in Go and refused here.
  11. Working with the borrow checker: fixes for when the compiler says no.
  12. Threads, Shared state, Channels, Send and Sync: goroutines.
  13. Async basics, then Async with Tokio: Go has no async/await; goroutines and channels do that job.
  14. Modules and Idioms and design patterns: packages versus modules, then common shapes.

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 says rustup target add installs 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’s std has no JSON; the usual answer is serde (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 in std have 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).
terminal
$ GOOS=windows GOARCH=amd64 go build -o demo.exe ./h04 && ls demo.exe
demo.exe
terminal
$ 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`
Gogo/json/main.go
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)
}
$ go run ./json
{"name":"apple","qty":3} <nil>

This page says nothing about compile times: they were not measured.

Each task edits a program above; the sessions below ran in the image.

  1. In habit 2, replace the match with find(2).unwrap(). It builds, then panics with called `Option::unwrap()` on a `None` value: Go’s nil crash again, but you wrote the word that allows it.
  2. In habit 8, change let _g = Guard(i); to let _ = Guard(i);. Each dropped line now comes before its loop line: _ does not bind the value, so nothing keeps it alive (Patterns).
  3. In the failing program of habit 9, put move on the closures and keep the plain count. It builds with two warnings and prints 0: each closure got its own copy of an i32. Safe Rust refuses data races; it does not refuse a program that gives the wrong answer.
$ cargo run
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
terminal
$ cargo run
dropped 1
loop 1
dropped 2
loop 2
dropped 3
loop 3
end of main
terminal
$ 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