Exploring Go's Type System vs Rust
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;
}
}
Feature | Go Structs | Rust Structs |
---|---|---|
Syntax | Fields and types are defined like normal variables | Fields are defined with type annotations |
Mutability | Fields are mutable by default | Fields are immutable by default; mut required for mutation |
Memory Safety | Relies on garbage collection | No garbage collection; uses ownership and borrowing for memory safety |
Default Values | Fields have zero values when not initialized | No default values; all fields must be initialized |
Methods | Methods defined with receivers (value or pointer) | Methods defined inside impl blocks with self |
Struct Composition | Uses embedding for inheritance-like behavior | No inheritance; composition via traits |
Passing by Value/Reference | Structs are passed by value unless a pointer is used | Structs are passed by value unless a reference (& ) is used |
Memory Management | Structs are garbage collected | Rust structs follow ownership and borrowing rules |
Trait Implementations | No built-in trait system | Supports 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();
}