Migrate to Rust from Go

In this chapter, we will look at some of the differences between Go and Rust. We will also look at some similarities between the two languages.

Both Go and Rust are modern programming languages that are designed to be fast, safe, and efficient. They are both statically typed and have a strong focus on concurrency and parallelism. There are some notable differences between the two languages, though, which we will explore in this chapter.

External packages

In Go, you can import external packages from the internet by adding them to your go.mod file. For example, to import the github.com/gorilla/mux package, you would add the following line to your go.mod file:

require github.com/gorilla/mux v1.8.0

Alternatively you can use the go get command to add a dependency to your go.mod file. For example, to add the github.com/gorilla/mux package as a dependency, you would write:

go get -u github.com/gorilla/mux

In Rust, you can import external crates from crates.io by adding them to your Cargo.toml file. For example, to import the rand crate, you would add the following line to your Cargo.toml file:

[dependencies]
rand = "0.8.4"

You can also use the cargo add command to add a dependency to your Cargo.toml file. For example, to add the rand crate as a dependency, you would write:

cargo add rand

Importing packages

In Go, you import packages using the import keyword. For example, to import the fmt package, you would write:

import "fmt"

In Rust, you import crates using the use keyword. For example, to import the std::io crate, you would write:

#![allow(unused)]
fn main() {
use std::io;
}

To use the previously imported rand crate, you would write:

#![allow(unused)]
fn main() {
use rand;
}

Async

Go has built-in support for asynchronous programming using goroutines and channels. Goroutines are lightweight threads that are managed by the Go runtime, and channels are used to communicate between goroutines.

Rust also has built-in support for asynchronous programming using the async and await keywords. Rust's async/await syntax is similar to that of other languages like C# and JavaScript. However, Rust does not have built-in support for channels like Go does. You need an asynchronous runtime like tokio or async-std to handle asynchronous programming in Rust. At the time of this writing tokio is the most popular asynchronous runtime for Rust.

A simple example of a goroutine in Go:

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        fmt.Println("Hello from goroutine")
    }()

    time.Sleep(1 * time.Second)
}

The same example in Rust using tokio:

use std::time::Duration;

use tokio::task;
use tokio::time::sleep;

#[tokio::main]
async fn main() {
    task::spawn(async {
        println!("Hello from tokio");
        sleep(Duration::from_secs(1)).await;
    }).await.unwrap();
}

Make sure to add the tokio dependency tokio = { version = "1", features = ["full"] } to your Cargo.toml file.

Channels

In Go, you can use channels to communicate between goroutines. Channels are a powerful feature of Go that allow you to send and receive messages between goroutines. Tokio provides similar functionality in Rust using the mpsc module.

A simple example of using channels in Go:

package main

import (
    "fmt"
)

func main() {
    ch := make(chan string)

    go func() {
        ch <- "Hello from goroutine"
    }()

    msg := <-ch
    fmt.Println(msg)
}

The same example in Rust using tokio:

use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);

    tokio::spawn(async move {
        tx.send("Hello from tokio".to_string()).await.unwrap();
    });

    let msg = rx.recv().await.unwrap();
    println!("{msg}");
}

Enumerations in for loops

In Go, you can use the range keyword to iterate over the elements of an array, slice, string, map, or channel. For example:

package main

import "fmt"

func main() {
    numbers := []int{1, 2, 3, 4, 5}

    for i, number := range numbers {
        fmt.Println(i, number)
    }
}

In Rust, you can use the iter method to iterate over the elements of a collection, and to get the index of the element you can use the enumerate method. For example:

fn main() {
    let numbers = [1, 2, 3, 4, 5];

    for (i, number) in numbers.iter().enumerate() {
        println!("{i} {number}");
    }
}

Traits

In Go, you can define interfaces to specify the behavior of a type. For example:

package main

import "fmt"

type Greeter interface {
    Greet()
}

type EnglishGreeter struct{}

func (eg EnglishGreeter) Greet() {
    fmt.Println("Hello, world!")
}

func main() {
    var greeter Greeter
    greeter = EnglishGreeter{}
    greeter.Greet()
}

In Rust, you can define traits to specify the behavior of a type. For example:

trait Greeter {
    fn greet(&self);
}

struct EnglishGreeter;

impl Greeter for EnglishGreeter {
    fn greet(&self) {
        println!("Hello, world!");
    }
}

fn main() {
    let greeter = EnglishGreeter;
    greeter.greet();
}

As you can see traits in Rust are similar to interfaces in Go. You can define a trait using the trait keyword and implement the trait for a struct using the impl block. The key difference is that in Go the interface is implemented implicitly, while in Rust the trait is implemented explicitly.

Public & Private visibility of functions and properties

In Go, functions and properties are public by default. To make a function or property private, you need to start the name with a lowercase letter. For example:

package main

import "fmt"

type Person struct {
    name string
}

func (p Person) GetName() string {
    return p.name
}

func main() {
    p := Person{name: "Alice"}
    fmt.Println(p.GetName())
}

In Rust, functions and properties are private by default. To make a function or property public, you need to use the pub keyword. For example:

struct Person {
    name: String,  // this is property is private
}

impl Person {
    pub fn get_name(&self) -> &str {
        &self.name
    }
}

fn main() {
    let p = Person { name: "Alice".to_string() };
    println!("{}", p.get_name());
}

Building for production

In Go, there is only one way to build your application: you run go build and it produces a single binary that you can run for development or production. In Rust, cargo build will produce a binary that is optimized for development. To build a binary optimized for production, you run cargo build --release.

The development binary is not optimized for speed, but it includes debug information that can be useful for debugging your application. The release binary is optimized for speed and does not include debug information.

You find the resulting binary in the target/debug or target/release directory.

In the Cargo.toml file you can further tune the release profile, for example:

[profile.release]
opt-level = "z"
lto = true

Reference material