I write Go in my daily work and I really like the language. Recently I started learning Rust, and I’m enjoying it a lot. I think learning a new language is fun and it also makes you a better programmer in your main language.
Every time you learn something new in the new language you catch yourself thinking “How do the other languages i know do this?” And with AI around making these comparison is easier and you end up learning a ton.
A side note before we start: people used to talk more about their experience learning a new language or a new tool. Now almost all the content is about AI, and honestly it gets a bit boring.
Some people ask, “Why bother learning new things when AI does it better?” I don’t buy that. You don’t walk up to a football fan and ask why he knows all these players. You don’t ask a movie lover why he remembers every little detail. they know because they love it. I enjoy programming so i don’t care if AI does it better than me or whatever. And after all if AI takes my job, I’m sure I’ll still be coding when I get home from the goose farm.
(That’s how I feel right now, and I hope it doesn’t change.) Sorry, I just used this blog post to leave a note for myself. Back to the story.
I was reading a book about Rust when I hit this sentence: “Rust uses usize for indexing.” I paused. Wait, indexing has a type? I had never really thought about it.
Type for Indexing
Rust
fn main() {
let arr: [i32; 3] = [1, 2, 3];
let i: usize = 1;
_ = arr[i]; // ok, the index is a usize
let j: i32 = 1;
// _ = arr[j]; // error: expected `usize`, found `i32`
_ = arr[j as usize]; // ok, but you must cast
}
- Index type:
- The index expression must be a
usize. usizeis an unsigned, architecture-dependent integer whose size matches the platform’s pointer size.
- The index expression must be a
- Why
usize?- An array index can never be negative, so an unsigned type makes sense.
- An index is not a memory address; it is an offset from the start of the array.
- When you write
arr[i], the compiler computes the element’s address asbase_pointer + i * size_of::<T>(). - So the index type must be large enough to represent the offset of any element in the largest possible allocation.
- Since the maximum allocation size is limited by the machine’s pointer size, Rust uses a pointer-sized unsigned integer (
usize).
- Bounds checking:
arr[i]is checked at runtime and panics ifiis out of bounds. The compiler may remove the check when it can prove at compile time that the index is always valid. The safe, non-panicking alternative isarr.get(i), which returns anOption<&T>. - Length and arithmetic:
arr.len()returns ausize(unsigned).arr.len() - 1when the array is empty (len == 0):- Debug build: panics (
attempt to subtract with overflow).1 - Release build: wraps to
usize::MAX.
- Debug build: panics (
- Because of this, idiomatic reverse iteration uses
(0..n).rev()orarr.iter().rev()instead of counting down fromn - 1.
What about
let y = 1; arr[y]? Isn’t1ani32? Integer literals are initially untyped, andi32is just the default when the compiler can’t infer a type. Since array indexing requires ausize,yis inferred asusize. No implicit cast happens. If you writelet y: i32 = 1, it won’t compile because Rust doesn’t implicitly cast between integer types.
Go
func main() {
var (
arr = [3]int{1, 2, 3}
x int8 = 0
y uint8 = 0
)
_ = arr[1] // 1 here is an untyped integer constant
_ = arr[x]
_ = arr[y]
}
- Index type:
- The index expression can be any integer type (
int,int32,int64,uint, etc.) or an untyped integer constant. - Go does not require a specific integer type for an index.
- The index expression can be any integer type (
- Why
int(signed)?- Indexes get used in plain arithmetic all the time, so Go keeps the type signed to avoid underflow.
- With an unsigned type,
len(s) - 1on an empty slice wraps to a huge number. With a signedintit is just-1, which is what you would expect.
- Bounds checking:
arr[i]is checked at runtime and panics ifiis out of bounds or negative. The compiler may remove the check when it can prove at compile time that the index is always valid. - Length and arithmetic:
len(x)andcap(x)returnint(signed).- Because it is signed, a reverse loop like
for i := len(s) - 1; i >= 0; i--just works, even on an empty slice.
- Because it is signed, a reverse loop like
Strings are different
- Go:
s[i]indexes the underlying bytes and returns abyte(uint8) not a rune.
A Go string is just a read-only sequence of bytes and strings are UTF-8 encoded sos[0]on"héllo"gives you the first byte, which may be only half of a character. - Rust: you cannot write
s[i]on astr/Stringat all. The language refuses this on purpose so you can’t accidentally split a UTF-8 character in half. Instead you can either slice a byte range,&s[0..4](which panics if the range doesn’t land on a character boundary), or iterate explicitly with.chars()(Unicode scalar values) or.bytes()(raw bytes).
So yes, indexing has a type. It’s cool to see these design decisions.
One sentence in a book, and now I notice it everywhere.
-
This is really an underflow (going below
0) but Rust just says “overflow” for both directions. (its kinda weird for me) ↩︎