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