What's in an ampersand? How learning Rust references first can make learning Go pointers harder
Contents
- Introduction
- Creating indirection with the ampersand
- Validity
- Mutability
- Declaring indirection
- Summary
Introduction
Was Rust the first language you learnt where you had to deal with pointers (and references)? Are you about to start writing some Go? Will you be dealing with pointers in Go? If yes to all these, this article might be for you!
I’m currently on a Go project and was finding a layer of confusion on top of anything to do with pointers, but I didn’t really know why. After reflecting a little, I realised this was because I was unthinkingly looking at Go pointer code through a Rust reference lens. As we’ll see, this is a recipe for confusion cake.
I wrote this article to summarise the differences and similarities that got in my way the most, with the hope that it can save someone else the confusion cake I was self-serving.
If you want to skip straight to the key things to know, head over to the summary at the end.
For the code examples from the article see the Go playground here, and the Rust playground here .
Creating indirection with the ampersand
Go and Rust both use the ampersand (&
) to indicate that something does not have direct access to the value itself and is instead holding an address to where that value is stored 1.
In Rust:
let greeting = String::from("Hello, World!");
let indirect = &greeting;
In Go:
greeting := "Hello, World!"
indirect := &greeting
Despite the shared syntax for creating indirection, the nature of the indirection is totally different.
In Rust the &
creates a reference, which is defined in The Rust Book as:
A reference is like a pointer in that it’s an address we can follow to access the data stored at that address; that data is owned by some other variable.
In Go the &
creates a pointer, defined more formally in A The Tour of Go with:
The & operator generates a pointer to its operand
The first way I encountered this distinction was when I printed the &
variables.
Rust gives you the value at the address:
let greeting = "Hello, World!";
let indirect = &greeting;
println!("{greeting}"); // Hello, World!
println!("{indirect}"); // Hello, World!
Go gives you the address:
greeting := "Hello, World!"
indirect := &greeting
fmt.Println(greeting) // Hello, World!
fmt.Println(indirect) // 0xc0000140a0
On seeing this, I inferred - without validating - that this meant Go’s pointers were more protective of the value at the address, that the indirection was somehow stronger and more robust.
But no, the opposite is true: Rust’s references are far stricter about the underlying value than Go’s pointers, as we’ll see.
This strictness manifests in how Rust and Go treat the mutability and validity of what’s behind the&
.
Validity
In Rust the compiler stops you from using a null or uninitialised reference: if your code is running then you can guarantee there will always be a value behind a reference i.e. it will always be valid.
Conversely, Go doesn’t give any guarantees about whether there’s actually something at the address pointed to by &
, and you will always have to check manually because it could be null i.e. invalid.
Trying to use a null reference in Rust, which won’t compile
let invalid: &i32;
// uncomment the below line to stop compilation with
// "error[E0381]: used binding `invalid` isn't initialized"
// println!("{invalid}");
Trying to create a null pointer in Go, which will compile but will give a null pointer error when you try to dereference the pointer
var invalid *string
fmt.Println(invalid) //<nil>
// uncomment below line, it will compile, but running will produce "panic: runtime error: invalid memory address or nil pointer dereference"
// fmt.Println(*invalid)
Mutability
Even when there is an underlying value, the differences continue in that value’s mutability.
In Rust, a reference’s underlying value is immutable. (You can only mutate the underlying value if you explicitly declare a mutable reference &mut
.)
let greeting = "Hello, World!";
let indirect = &greeting;
// uncomment the below line to stop compilation with
// "error[E0596]: cannot borrow `*indirect` as mutable, as it is behind a `&` reference"
// indirect.push_str("It's me!");
let mut mutable_greeting = String::from("Hello");
let mutable_indirect = &mut mutable_greeting;
mutable_indirect.push_str(", World!");
println!("{mutable_indirect}"); // Hello, World!}
As you may have guessed, Go places no restrictions on mutating the values behind pointers 2.
greeting := "Hello, World!"
indirect := &greeting
*indirect = "World, Hello!"
fmt.Println(greeting) // World, Hello!
fmt.Println(*indirect) // World, Hello!
These two differences caught me out so often:
- Validity: I’d be looking at the
&
in some Go code and it wouldn’t cross my mind to check there’s something at the address and next thing I know I’d get hit withinvalid memory address or nil pointer dereference
. - Mutability: I’d also be doing double takes whenever mutation was happening around a pointer, looking for
&mut
but only seeing&
Declaring indirection
The &
in Rust and the *
in Go
The final difference is not huge, but you could call it the cherry on the confusion cake:
- Rust uses the
&
for creating indirection and describing the type. - Go uses the
&
for creation indirection but the asterisk*
for describing the type.
Rust using the &
to declare and describe the type
let greeting = String::from("Hello, World!");
let indirect = &greeting;
println!("{}", std::any::type_name_of_val(&greeting)); //alloc::string::String
println!("{}", std::any::type_name_of_val(&indirect)); //&alloc::string::String
The &
is also used declaring type signatures, this function takes a reference to a string:
fn count_bytes(s: &String) -> usize {
s.len()
}
Whereas in Go although you create a pointer using &
, when you’re describing it you use the de-reference operator *
greeting := "Hello, World!"
greetingIndirect := &greeting
fmt.Println(reflect.TypeOf(greeting)) //string
fmt.Println(reflect.TypeOf(greetingIndirect)) //*string
This function takes a pointer to a string:
func countBytes(s *string) int {
return len(*s)
}
I was only used to seeing the *
operator in the narrow context of dereferencing a raw pointer in Rust. So, when I saw these types in Go I thought ‘Oh it’s a de-referenced value, so it was behind a pointer but now we’re getting the real thing’. Seeing that in writing does make me wonder why I thought it, but as I said this was more the cherry on the cake than the cake itself.
Summary
These are the key differences and similarities between Rust’s references Go’s pointers, hopefully it saves someone some time:
Rust Reference | Go Pointer | |
---|---|---|
Created with | & | & |
Type declared with | & | * |
Stores address to value? | Yes | Yes |
Always valid? | Yes | No |
Can mutate underlying value? | No | Yes |
Further reading
Rust Pointers for C Programmers
What's the difference between references and pointers in Rust? | nicole@web
Smart Pointers - The Rust Programming Language
Arguably Rust’s
String
type, being a smart pointer, is sort of between pointer and ‘underlying value’, but I’m putting that ambiguity aside as it’s a useful type to demonstrate with.↩As strings are immutable in Go, you’re not technically mutating the underlying value, because Go creates a new string, but for most use cases the end result is as if you’ve mutated the original↩