After starting to dive into learning Go over the last six months, being one of the fundamental building blocks of a Go application I found myself using structs a lot. This spurred me to take a deep dive into exactly what a struct is, how they are represented in memory, and how to get the most out of structs.
This is my insight into exactly that.
What is a struct?
Structs, at their most basic level, are simply a collection of properties grouped into one type.
This allows all the data relating to one entity to be neatly encapsulated in one lightweight type definition, behaviour can then be implemented by defining functions on the struct type. In the below example a function has been created to mutate the private username field of the
User struct, performing some validation on the updated value.
Using a Struct
Extending on the previous example of the
User struct. An instance of a struct can be created and used like in the below example.
Any fields omitted from when instantiating a struct will take on the zero value of that field's type. E.g. if age was omitted when creating a user the default value is
Once a struct has been instantiated, any available functions defined on the struct type can be called on the variable that the struct was assigned to.
Go provides optional string literal struct tags that can be used to add metadata to a struct field. Tags are especially useful when it comes to marshalling and unmarshalling data from formats like
To extend on the same
User struct example as previous sections, the below example adds JSON struct tags and un-marshals a JSON document containing the user data directly into the User struct.
The same can be done for a JSON document containing an array of users by simply modifying the users variable to be an array of User.
This page maintains a good list of well-known struct tags for marshalling/unmarshalling from different formats.
When it comes to memory allocation for structs, they are always allocated contiguous, byte-aligned blocks of memory, and fields are allocated and stored in the order that they are defined.
The concept of byte-alignment in this context means that the contiguous blocks of memory are aligned at offsets equal to the platforms word size (4 bytes on a 32-bit, 8 bytes on a 64-bit system). Consider the following example of a struct where there are three fields each of varying sizes, on a 64-bit environment blocks of memory will be aligned at 8 byte offsets.
This results in the first block of 8 bytes being fully occupied by
a (8 bytes). The next block of memory (offset by 8 bytes from the starting memory address of the struct) has its first 2 bytes occupied by
b, the next 1 byte occupied by
c then the remaining 5 bytes are empty pad bytes.
Considering how memory is allocated for structs as seen in the previous section, depending on the order that fields are defined in a struct it can be rather inefficient due to the number of pad bytes required. It is possible to optimise the memory utilisation of a struct however, by defining the fields in a deliberate order to maximise the use of each block of memory, reducing the need for redundant pad bytes.
The following example there is a struct
Post representing a blog post. In the first iteration, before taking steps to optimise its memory utilisation the total memory of the combined fields totals 35 bytes, however, the total struct size equates to 48 bytes due to pad bytes.
Now, if the struct fields are re-arranged to minimise padding bytes, the resulting struct size is only 40 bytes.
In modern systems where memory constraints are not typically an issue, the benefit gained from micro-optimisations like this, reclaiming 8 bytes of memory is not enormous. However, the ability to understand at this level, how a struct is allocated memory, and how to, if required apply such optimisations is invaluable.
Code examples from this post are available at the below links in the Go Playground.