Coding with Go Structs
Overview #
Structs allow us to group data together in Go. This is similar to other programming languages’ classes, data objects, or records.
A struct can do two main things, group data and be used as a type to implement interfaces.
Structs can also contain metadata in the form of “tags” that frameworks can use for validation and serialization.
The syntax for a struct is
| |
Below is an example of a simple struct:
| |
And another with generics:
| |
We can instantiate a struct using the struct literal syntax.
| |
Note: Struct and attribute names should start with a capital letter to make them visible outside the current
package.
Note: When initializing a struct, you don’t need to provide values for all the struct’s attributes.
Zero Value Instantiation #
A Struct behaves the same as any other variable in Go; It has a zero value and can be referenced by a pointer. A Struct’s zero value is an empty struct with attributes/fields set to zero values corresponding to each type.
This means if we declare our Person struct as:
| |
The Person struct is instantiated in memory, and its Name and Age string fields are set to "".
Structs have a literal syntax using curly brackets, and because go does not enforce specifying all struct attributes, we
can instantiate an empty
struct using Person{}.
There is also the new function, which instantiates a type to its zero value and returns a pointer:
| |
Which one you use is up to you, but I prefer the literal syntax and only use the zero value instantiation when I want to build and set the struct values in a function.
Adding Methods #
Object-oriented programming wants to add methods to everything, attaching functionality with data.
Data-oriented and functional programming separates data from functionality. I personally prefer to only add methods in
particular cases
like To String, Comparators and when required for polymorphism.
Methods are added differently than most mainstream languages. Instead of defining it inside the struct declaration, we define a method independently.
The syntax is:
| |
The receiver type can be a pointer or a value type. I would suggest to stick with pointer types and only use value
types when you have the need.
For a good example, see A Tour of Go: Methods.
We can access the receiver’s attributes by giving it a name and then using the dot notation <receiver>.<attribute>.
Below is an example of writing a String() method for the Person struct:
| |
Methods in different files from the type #
Methods and types can live in different files as long as they are in the same package.
We need to use a type definition to define a method for a type in a different package.
| |
Trying to do so without the type definition will result in
a Invalid receiver type '<...>' ('<...>' is a non-local type) compilation error.
Struct Constructor Methods #
Structs have no constructor methods as found in mainstream OO languages, nor are they needed. A simple function is all we need without adding complex constructor semantics to our cognitive burden.
In Go, it is common to see constructor functions prefixed with “New”. The naming is purely convention and does not affect how the Go compiler sees the function.
Here is an example of a constructor creating a new node. Note: the example makes use of generics.
| |
Implementing interfaces #
Polymorphism is a way that we can separate behaviour from the actual implementation. It introduces a layer of indirection where we need more flexibility as to what implementation is used, especially when writing libraries or utility code.
In Go, interfaces are defined almost the same as with structs, with the exception that the body of an interface only contains functions.
For example, see the Stringer interface, which the fmt package uses to abstract how types are printed.
| |
In Go, we do not mark a struct as implementing any interface but instead, just add the interface’s method to the type.
If we wanted our Node struct to implement the Stringer interface, we only need to implement the method for it like
below:
| |
Now, the Node struct can be passed to any function that requires the Stringer interface.
Combining Interfaces (Embedding) #
It is idiomatic in Go to define very finely-grained interfaces. Interfaces can be built up by defining new interfaces as the combination of several simpler interfaces.
Let’s say we wanted to define an interface for a HashTable key. We want the key to be hashable and also
comparable.
We can define a Hashable interface and a Comparable interface, and finally combine both into
a HashTableKey interface.
| |
And we can use it like:
| |
For more information, see the official documentation Go embedding.
Telling if a struct is empty #
Golang has many ways to test if a struct was initialized (not empty). Which one do you use? It depends on how much do we want our code to know about the type we are testing?
Using reflection #
This is generic and works for any type, i.e. the testing code does not need to know about the struct’s type.
Using reflect.ValueOf(p).IsZero() returns true if p is empty and false otherwise.
To demonstrate, the code below will print Is Empty and Not Empty.
| |
Using reflect lets us assume nothing of the struct we are testing, and we can utilize the interface{} type. This is perfect for library or utility code.
Using a struct field #
Testing if a field is set or not. This is the easiest way but does require knowledge of the struct’s type.
| |
Using a zero value #
This method requires knowledge of the struct’s type. It also places an extra restriction that the struct
must contain only comparable values. This means if Person contained a slice field this method would
result in a compile error.
if p == (Person{}) {
fmt.Println("Is Empty")
}
| |
On using deep equal #
This method requires knowledge of the struct’s type and compares each field.
| |
On using nil #
In many programming languages, null, None or nil is used to test for empty objects. In Golang, we can use nil only
on pointers,
but we cannot use them on any other types.
To demonstrate, if you try to compile the following code, you will get a cannot convert nil type error.
| |
The variable p of type Person is always instantiated when first declared but has not been set with values.
The following uses a pointer, and the code does compile.
| |
Compiling code does not equal working as intended code. We should only use this if we want to check that a pointer has a value because we can also, cheat in do:
| |
The statements var p Person and p := &Person{} are equal, with the only difference being that the second assigns a
pointer to p, but
both create (instantiate) an uninitialized version of the Person struct.
Struct Tags (JSON and YAML support) #
Struct tags are used mostly for serialization to and from JSON, YAML or XML formats. It can be used and interpreted by any framework and is a way to put metadata on each attribute.
The syntax is:
<name> <type> `<tag key>:"<tag value>"[ space ]<tag2 key>:"<tag2 value>"`
Go’s attribute visibility works by defining the first letter of an attribute as an upper-case or lowercase letter. But this creates an issue when reading and writing JSON data because JSON attributes are almost always lowercase.
For example:
Then JSON string
| |
It would not marshall correctly into our Person struct.
For this to work, we need to add JSON tags to our struct so that it knows which lowercase attribute matches which
attribute in the struct.
| |
We can add more tags to each attribute by simply separating them with a space:
| |
Important: tags are separated with a space, not a comma.
Anonymous Structs #
Anonymous Structs have no name, and cannot be reused outside of the context in which they are declared. We use them to provide typing to a bunch of data where we do not want or need to reuse the type again.
This is especially useful when specifying a list of records for testing.
In the example below, we define a list of records with attributes, id, name and age. Any code that accesses the records slice can use the dot notation to read the properties.
| |
Note: An anonymous struct is exactly the same as a named struct for all purposes. It just doesn’t have a name.
Anonymous struct definitions are valid type declarations. This means you can create some uncallable code that will compile but the type system will not allow you to do much because the type is unknown outside the declaration’s context.
| |
| |
Combining Structs (Embedding) #
In Golang, composition is used instead of inheritance, and embedding is a shortcut method for composing different types.
For example, let’s say we wanted to add the Identity type to our Person struct:
| |
We can use the IdLen method on Person as if we defined it for that struct. We can also refer to the Id
field
in the Identity struct.
For example:
| |
It is also possible to refer to the embedded type using the type name like so:
| |
For more information, see the official documentation Go embedding.
Summary #
My intention for this post was to serve as a one-stop reference for all things struct. It is not an exhaustive reference, but I hope that it reflects 99% of what a Go programmer needs to know about structs.
My advice to any programmer would be to practice the fancier methods, but when writing code, always keep things the simplest and most readable as possible.



