Skip to main content

Exploring Go's Type System vs Rust

· 7 min read

I have been hearing a lot of hoopla about Rust's type system and how it is better than Go's from a bunch of crusty rustaceans. I realized that although I have used Go for some projects and really enjoy the language, I do not understand the type system as well as Python's and C's. I would like to do a deep dive, and compare Rust to Go from a beginner's perspective.

Primitive Type

Scalar Types

Integers

Rust

  • i8, i16, i32, i64, i128: Signed ints for different bit sizes
  • u...: the same thing but unsigned ints for different bit sizes
  • isize/usize: int types with architecture dependent sizing (used for indexing collections)

Go

  • int, int8, int16, int32, int64: Signed ints for different bit sizes
  • unint...: same as before but unsigned

Both languages have fixed-size ints for better control over memory.

Floats/Complex

Rust

  • f32,f64: 32 and 64 bit floating point numbers

Go

  • float32, float64: the same as Rust
  • complex64, complex128 floats that can represent real and imaginary parts

Boolean

Rust

  • bool: true and false

Go

  • bool: true and false

Char/Rune

Rust

  • char: a single Unicode scalar value (special characters/emojis as well)

Go

  • rune: a single Unicode scalar value or a surrogate pair outside the Basic Mutilingual Plane

It is important to note that the char in Rust is guranteed to be a valid Unicode scalar point, but the rune in Go can be any Unicode code point. They are both 4 bytes. Go's rune is more flexible and can represnt a wider range of Unicode code points, but you have to check the validity of the Unicode scalar point values.

Rust Example:

let valid_char: char = 'A'; // Valid Unicode scalar value
let invalid_char: char = '\uD800'; // Invalid Unicode scalar value (lone surrogate)

// This will compile without errors:
println!("{}", valid_char);

// This will result in a compile-time error:
println!("{}", invalid_char);

Go Example:

var validRune rune = 'A' // Valid Unicode code point
var invalidRune rune = '\uD800' // Invalid Unicode scalar value (lone surrogate)

fmt.Println(validRune) // Output: A
fmt.Println(invalidRune) // Output: 55296 (the numerical value of the invalid code point)

Composite Types

Composite data types are data types that are constructed from combinations of primitive data types. They allow you to represent more complex structures and relationships between data elements.

Arrays

Arrays in Go and Rust both work the same: fixed-size collectinos of elements of the same type. Both support slicing, and passed by value (Rust has ownership rules). Rust arrays are immutable by default but can be made mutable using the mut keyword. Go arrays are mutable by default, and does not follow strict borrowing rules. It can be modified freely unless passed as a parameter then it is passed by value and results in a copy being made.

Rust Example: Mutable Array

let mut arr = [1, 2, 3];
arr[0] = 10;

Go Example:

arr := [3]int{1, 2, 3}
arr[0] = 10

Go has a garbage collector so we do not need to worry about deallocating and cleaning up ourselves, unlike Rust. That being said, Rust does use the stack as the default storage for arrays. Arrays in Go are value types, meaning when we pass it to a function it will be copied unless we use pass by reference.

Slices

Slices in Go are dynamic views of arrays and more commonly used. Go slices are more related to vectors as they can grow and shrink in size as needed. In Rust, slices are references to the data they point to and they do not own the data. Their size is dynamic and determined at runtime.

Go Slice Example:

arr := [3]int{1, 2, 3}
slice := arr[1:]

Rust Slice Example:

fn main() {
let mut arr = [1, 2, 3, 4, 5]; // Mutable array

let slice = &mut arr[1..4]; // Mutable slice
slice[0] = 10; // Modify the first element of the slice
println!("{:?}", arr); // Output: [1, 10, 3, 4, 5]
}

Struct

Both languages support structs or groups related data. They differ in features, memory management (surprise!), and flexibility.

Go Example

// Simple struct
type Person stuct {
Name string
Age int
}

// Instantiation and usage
person := Person{Name: "Alice", Age:30}
fmt.Println(person.Name)
person.Age = 31

// Methods on structs
func (p Person) Greet() string {
return "Hello, " + p.Name
}

// Pointer and Mutability
func (p *Person) HaveBirthday() {
p.Age += 1
}

In Go, structs are passed by value. If we want to modify the data in the struct, we must use a pointer.

Rust Example

// Struct definition
struct Person {
name: String,
age: u32,
}

//Instantiation and usage
let person = Person { name: Sting::from("Alice"), age: 30}; // ugly ass syntax
println!("{}", person.name);
let mut person = person;
person.age = 31;

// Methods on structs
impl Person {
fn greet(&self) -> String {
format!("Hello, {}", self.name)
}
}

//Ownership and borrowing Methods
impl Person {
fn have_birthday(&mut self) {
self.age += 1;
}
}
FeatureGo StructsRust Structs
SyntaxFields and types are defined like normal variablesFields are defined with type annotations
MutabilityFields are mutable by defaultFields are immutable by default; mut required for mutation
Memory SafetyRelies on garbage collectionNo garbage collection; uses ownership and borrowing for memory safety
Default ValuesFields have zero values when not initializedNo default values; all fields must be initialized
MethodsMethods defined with receivers (value or pointer)Methods defined inside impl blocks with self
Struct CompositionUses embedding for inheritance-like behaviorNo inheritance; composition via traits
Passing by Value/ReferenceStructs are passed by value unless a pointer is usedStructs are passed by value unless a reference (&) is used
Memory ManagementStructs are garbage collectedRust structs follow ownership and borrowing rules
Trait ImplementationsNo built-in trait systemSupports traits for behavior reuse across types

OOP

Both languages are not object oriented in the tradditional sense. Rust still adheres to the core pricniples of OOP through traits. Go is also object oriented-like through the use of interfaces.

Go OOP Example with Interfaces:

type Animal interface {
Speak()
}

type Dog struct {
Name string
}

func (d Dog) Speak() {
fmt.Println("Woof!")
}

type Cat struct {
Name string
}

func (c Cat) Speak() {
fmt.Println("Meow!")
}

func main() {
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}

var animal Animal

animal = dog
animal.Speak()

animal = cat
animal.Speak()
}

Rust Example with Traits:

trait Animal {
fn speak(&self);
}

struct Dog {
name: String,
}

impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}

struct Cat {
name: String,
}

impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}

fn main() {
let dog = Dog { name: "Buddy".to_string() };
let cat = Cat { name: "Whiskers".to_string() };

dog.speak();
cat.speak();
}