Arrays in Go
My notes on arrays in Go
Array lay out foundation for slice in Go. Array is a list of fixed size of similar kind of elements that stores in contiguous locations in memory. Go's arrays share some similarity with C but Go adds some extra layers to prevent misuse of an array like we cannot do pointer arithmetic in idiomatic Go.
To create an array of type T, the syntax looks something like this.
The zero value of array is the zero value of the T. So, every element of an array is set to whatever the zero value of T is.
Below snippet creates an array of integer with size 3. Because zero value of int is 0, Go initializes every element to 0.
The size of an array must be known by the compiler, so as long as compiler can figure out the size, we can use it. For example, we can use any constant expression like this.
We cannot use var here, because a variable created with var is mutable, so in below case Go is not happy with it.
Compiler will give invalid array length totalSlots.
As always, we can use subscript notation to access elements of an array and can update them as well.
Go's compiler can catch error for out-of-bounds access as long as it can statically analyze them. So in below case compiler will throw an error invalid argument: index 256 out of bounds [0:128].
But we can screw it up at runtime like this.
This will give us panic: runtime error: index out of range [256] with length 128.
Go is a type-safe language, meaning it has no undefined behavior like C has. So, Go adds some extra safety checks behind the scenes. In the following innocent program, Go is still checking for out of bound access and prevents the program from accessing the memory location that does not belong to it.
Above code will prints five zeros and then panic with error message panic: runtime error: index out of range [5] with length 5.
Array Literal
There are many ways to create and initialize an array. Here are the all possible ways to do it.
Here are the examples of above syntax.
A few things to remember when using sparse index syntax.
Any subsequent values after index:value pair are going to placed after index. So, for example in settings array, after 10: Labels the Status and Team will be placed at 11 and 12 index corresponding.
On the other hand in tenants array, after 4th index the Victor Sullivan goes to 5th index but then we add value for 3rd index. This is allowed because 3rd index is not specified before. If we change it to 2 then the compiler will complain about duplicate index 2 in array or slice literal. So, compiler is helping us to prevent any accidental modification to existing index.
Comparison
In Go, we can directly compare two array variables if their sizes are the same and element's type is comparable. Remember, size of an array is part of its type.
In above example, both arrays have the same size and same element type. So, it is legal to compare them. If the sizes were different, the comparison would produce an error. Here, Go eliminates comparing two arrays of different size at compile time.
Here we will get compilation error invalid operation: a1 == a2 (mismatched types [3]int and [4]int)
Arrays Are Values
One more thing about arrays in Go is that they are values rather than a pointer to first element like in C and C++. So when we assign an array to a new variable or pass it as a function argument, Go creates a copy of that array and uses it. So, like a normal value, assignment creates a copy.
This is different then C because in C, array decay into pointers so when we pass array into function, an address to the first element is actually passing rather than copy.
In above code, a and b are two different arrays in memory. So, mutating one does not affect other. Similarly, when b is passed to incrementByOne, it receives a new copy of b so any changes the function made do not affect the original array.
Pointers and Arrays
Because arrays are values, passing a large array to a function is costly because it needs to create a new array and copy all the elements from original. We can use pointer for this scenario and Go makes it easy to work with pointers.
Here the interesting part is a[i] in incrementByOne. a is actually a pointer but Go simplifies the syntax to work with pointers. It is just syntactic sugar over (*a)[i].
One more thing here is that the zero value of a pointer is nil. So, when working with pointers we need to make sure we don't dereference a nil pointer which causes a runtime panic.
Go does not allow pointer arithmetic directly on an array. This code will fail to compile with an error: invalid operation: pa + 1 (mismatched types *[3]int and untyped int)
Although there are dark magic like this.
Let me explain this scary looking thing.
p := uintptr(unsafe.Pointer(&a)) this takes the address of a which is address of first element's first location. Then, we convert that address to unsafe.Pointer because in type safe Go, it does not allow any kind of pointer arithmetic. Then we convert Pointer into uintptr, which produces the memory address as an integer. uintptr belongs to the integer family, and it bypasses all the type safety that Go provides.
One thing to note here, uintptr is an integer and not a reference. So it does not have any pointer semantics which means we cannot dereference the variable p, even if it contains a valid memory address. Also, the garbage collector is not happy to track uintptr variables. So, idiomatic Go does not allow uintptr or unsafe package at all.
val := *(*int)(unsafe.Pointer(p)) in this statement, we convert p, which is uintptr, back to Pointer; so we can dereference it. To read value correctly we tell compiler to treat that pointer as *int because a is holding int type elements. At this point the type becomes a pointer to an integer(*int), because we are interested in value we just dereference it and store the value in val variable.
p = p + unsafe.Sizeof(a[0]), here we are adding offset to p. We cannot do p + 1, otherwise it adds only a 1 byte offset. We want to move to next location in array so we need to move by the size of integer which we can get by using unsafe.Sizeof and here we pass first element as expression.
Len and Cap
The built-in len and cap functions return the size of the array.
Limitations of Array
Array has one major limitation, its size. We cannot write a program like this.
It will fail to compile with an error invalid array length size. Because in its definition, size is a required parameter which must be known by compiler at compile time.
To address this limitation, Go provides an abstraction over array in form of slice which is Go's way to do dynamic array.
Footnotes
From the book Programming Rust - FAST, SAFE SYSTEMS DEVELOPMENT by Jim Blandy & Jason Orendorff, they answer the question: What do we mean by type safety?. So it all starts from the definition of undefined behavior from 1999 C standard call C99.
It means the program can do anything.
Go is type safe language. Go puts checks like out of bound index access for example that prevent the program from exhibiting to do any undefined behavior.
If a program has been written so that no possible execution can exhibit undefined behavior, we say that program is well defined. If a language’s safety checks ensure that every program is well defined, we say that language is type safe.