A struct, or structure, is a composite type made up of other basic types or composite types. It allows us to group logically linked properties about an entity into a single representation of the entity.
The struct type itself is not unique to the Go programming language; you’ll find structs in languages like C and many of its derivatives and a struct is similar in many ways to a class, something found in object-oriented languages.
The difference between how you use a struct in Go versus a class in an object-oriented language is subtle but is often summed up like this: Go favours composition over inheritance.
In fact, Go does not support inheritance. More later.
The struct is an incredibly useful and flexible type. You will see and use it often in Go code. Here’s a very basic struct to represent an Animal with just one property, Age of type int.
type Animal struct {
Age int
}
Creating Instances
For a struct to be useful in our code we need an instance of it, in much the same way we need an object of a class in object-oriented languages.
We can create a struct in three ways, and I’ve illustrated with three examples below.
In the first example, we create a pointer to an instance with the new keyword.
// Example 1
animal1 := new(Animal)
We haven’t discussed pointers so far, and we’re not going to deep-dive here. But a pointer is a value that is a memory address. So, animal1, instead of holding the value of an Animal struct actually holds the address where this value is stored in memory.
In the second example, we use a struct literal to achieve “almost” the same thing.
// Example 2
animal2 := Animal{}
Here animal2 holds a value of the struct type Animal, and this despite appearances, is not the same thing as in Example 1.
Try this in the Go Playground which prints the contents of the variables animal1 and animal2: https://play.golang.org/p/6YkpzWezWeM.
In the final example, we create an instance of an anonymous struct, where we define the struct inline at the point we need to use it.
// Example 3
animal3 := struct{Age int}{}
Again, we store a value and not a memory address.
At first sight, this would not appear to be an Animal - it is not created from the Animal struct after all - but Go is able to infer that these values, which are both empty values at this time, are the same.
Try it for yourself in the Go Playground: https://play.golang.org/p/f3qqOdLxXDY.
Setting and Getting data
Creating instances of empty structs is all well and good, but not very useful.
So, how do we set the values of struct instances by altering their properties?
We can approach this in a couple of ways, initialising properties when creating the instance or, after-the-fact, by changing them later.
We’re going to build on the same examples used above but modify the Animal struct with a couple of additional properties.
type Animal struct {
Age int
Diet string
Notes string
secret bool
}
In our first example, we create a pointer to an Animal struct. Properties must be set after-the-fact, using the dot separator illustrated in the code below.
// Example 1
animal1 := new(Animal)
animal1.Age = 4
animal1.Diet = "Grass"
animal1.Notes = "Likes the dark"
fmt.Println(animal1.Age)
// Output
4
In the second example, we created an Animal using a struct literal. Using this approach we have the ability to initialise properties with our own assignments at the time we create it, and we can do this in two ways, naming the properties explicitly as in Example 2a, or letting the compiler infer the properties as in Example 2b.
The thing to note here is that if we let the compiler infer the properties, we need to supply a value for all properties and in the correct order, (the same as the order as in the Animal struct type) whereas if we used named properties, we are free to provide any or all properties, and you will note, in any order.
// Example 2a. Named properties
animal2a := Animal{
Diet: "Grass",
Age:5,
}
fmt.Println(animal2a.Age)
// Output
5
// Example 2b. Unnamed properties
animal2b := Animal{6,"Grass","Likes the dark", true}
fmt.Println(animal2b.Age)
// Output
6
The last thing we should mention here is that we can still alter properties using the dot notation as in Example 1, post-initialisation.
animal2b.Age = 7
fmt.Println(animal2b.Age)
// Output
7
In the final example, we used an anonymous struct. The struct is both defined and initialised inline, and both named and unnamed properties can be used.
For completeness, both approaches are shown using anonymous structs.
// Example 3a. Anonymous with named properties
animal3a := struct{
Age int
Diet string
Notes string
secret bool
}{
Notes: "Loves grass",
Age: 8,
}
fmt.Println(animal3a.Age)
// Output
8
// Example 3b. Anonymous with unnamed properties
animal3a := struct{
Age int
Diet string
Notes string
secret bool
}{
2,
"Grass",
"Likes the dark",
true,
}
fmt.Println(animal3b.Age)
// Output
2
Again, we can make new assignments to properties post-initialisation using syntax like the below.
animal3b.Age = 10
fmt.Println(animal3b.Age)
// Output
10
Exported or Unexported
When we talked about functions in the last tutorial, you may recall that we covered how to make things public or keep them private when creating packages. The difference was subtle but semantic and very simple; the capitalisation of the name itself was used to make things visible, or invisible, outside of the package.
Struct naming follows this same pattern; with a twist. Not only can we denote exported/unexported with name capitalisation on a struct, but we can also use the same pattern to make a struct’s properties, public or private.
In the previous examples, our Animal had one property that we largely ignored, “secret” with a lower case “s”. This means the property, whilst available to be set and get in the same package, is actually secret outside of the package.
As with functions and receivers, this provides a useful feature for ensuring you control the public interface to your package by keeping aspects of your packages’ implementation private.
Let’s look at a few examples.
// Example 1. Exported struct with exported property, Age
type Animal struct {
Age int
}
// Example 2. Exported struct with unexported property, age
type Animal struct {
age int
}
// Example 3. Unexported struct with exported property, Age
type animal struct {
Age int
}
Example 3 requires a little more discussion. Though the struct itself is unexported it contains an exported property.
This would seem to be useless at first glance, but here we should note that there are two ways to initialise struct types from outside of a package, directly or indirectly by returning an instance from within the package itself.
This example shows the two different methods on the Go Playground: https://play.golang.org/p/CDX-bMHySiW. Uncomment the lines to test the two approaches.
The first method fails as it tries to refer to an unexported type directly. The second approach succeeds since an exported function returns in an instance of an unexported type, which itself has exported properties we can access.
You should probably avoid this though. It breaks the package contract and can be confusing.
Linting tools will also often flag unexported return types at compile-time.
Receivers
In the functions tutorial, we mentioned receivers very briefly. At the time I described them as a special type of function that receives the value of a type. That’s a good enough definition even for now, but to make the comparison with object-oriented programming once again - if that’s your background - receivers are similar to the methods of a class.
Receivers can be one of two kinds: value receivers or pointer receivers. We’ll illustrate with examples as always, but the distinction is this: a value receiver can use the value of a type, but not modify it, since it receives a copy. A pointer receiver can both use the value of the type it receives and modify its value since it receives a reference to the original value and not a copy.
Since we are discussing receivers in the context of structs, the value comprises all of the properties of the struct.
Let’s illustrate the syntax and the differences will become obvious.
type Animal struct {
Age int
}
// GetAge is a value receiver, it
// receives a copy of an Animal instance
func(a Animal) GetAge() int{
return a.Age
}
// SetAgeNoworky, changes a copy of animal
// not the original, it doesn't work like you expect
// since it is a value receiver
func(a Animal) SetAgeNoworky(age int) {
a.Age = age
fmt.Println("Inside value receiver:", a.Age)
}
// SetAge is a pointer receiver, it
// receives a reference to the Animal instance
// so can modify it. Notice the asterisk.
func(a *Animal) SetAge(age int) {
a.Age = age
}
func main(){
animal := new(Animal)
animal.Age = 7
fmt.Println(animal.GetAge())
// Output
7
animal.SetAge(10)
fmt.Println(animal.GetAge())
// Output
10
animal.SetAgeNoworky(20)
// Output
Inside value receiver: 20
fmt.Println(animal.GetAge())
// Output (still 10)
10
}
There’s a link to this code on the Go Playground so you can explore the distinction between value receivers and pointer receivers for yourself: https://play.golang.org/p/khbJ4sVdTKU.
Mostly it doesn’t matter too much which you use, but it’s most common to want to operate on the instance itself, and not a copy, so in the majority of cases, a pointer receiver is the one to use.
Some people don’t like to see value receivers mixed with pointer receivers but I think mixing them allows us to communicate the intent of the receiver and the package API very effectively. Can we expect the receiver to modify the original value, or simply use a copy of the original value.
You will decide which style you prefer.
Composition
Composition is relevant to structs and interfaces. We’ll revisit when we cover interfaces; the principles are the same for both structs and interface types.
We mentioned earlier that in Go we favour composition over inheritance, and that we do not actually have inheritance in Go.
So how does composition differ from inheritance, a concept familiar in object-oriented languages?
It’s similar, but definitely not the same. With inheritance, as implied, we inherit behaviours and properties from base classes, building the object we need for our use case by extending the functionality of parent classes using child classes.
This works, but has a few problems, particularly in languages that do not allow multiple inheritance. We can end up with duplication where we need objects that extend and inherit behaviour from more than one class.
Composition allows us more flexibility. We can compose structs from other structs to provide the type representations we need.
The remainder of this section will provide examples with commentary where elaboration is helpful.
Let’s stick with the Animal struct example, but refactor it a little to make it more practical.
type Animal struct {
SpeciesName string
Age int
Diet string
}
With this type, we can represent common properties of any animal. They all have a species, they all have an age, and they all eat something.
Now suppose we want to represent Canines, a collection of sub-species, with specific properties.
If we are trying to compose a type to represent a Canine, that Canine is still an animal, and so the properties of Animal are relevant.
So we create a second struct to represent the additional properties of a Canine, and embed the Animal struct into it.
In our example, we will include just one additional property, “Domesticated” a boolean.
type Canine struct {
Animal
Domesticated bool
}
So we can now represent Canines fairly well. They are all animals, of a species, that have an age, and diet, but some are domesticated, and some are not.
Later, we decide we need a type to represent a Dog.
A Dog is a Canine, which is an Animal, so we can create a new struct type Dog and embed the Canine struct, but it has some additional properties, one of which could be a “Vaccinated” property. We’ll use this.
type Dog struct {
Canine
Name string
Breed string
Vaccinated bool
}
Embedding in this way makes the embedded struct part of the host struct. The properties of the embedded struct can be used as if they belonged to the host. This is similar to inheritance, but not the same - we’ll illustrate this point shortly.
First though, let’s create a new Dog and set all of its properties, both those of the Dog struct, those proper to the Canine struct which is embedded, and those proper to the Animal struct, embedded in the Canine struct.
myDog := new(Dog)
// set the properties
myDog.Name = "Rolo"
myDog.Vaccinated = true
myDog.Domesticated = true
myDog.Breed = "Cocker Spaniel"
myDog.Age = 5
myDog.SpeciesName = "Canidae"
myDog.Diet = "Pedigree Chum"
Notice here, how it’s no different, whether the property is common to the Dog struct or a struct which is embedded?
Have a play with this example on the Go Playground: https://play.golang.org/p/Ov6cp9dS_oa.
But, just to confuse matters, we can also express the relationship between the structs and their properties when setting and getting more explicitly.
As in this example.
myDog2 := new(Dog)
// set the properties
myDog2.Name = "Rolo"
myDog2.Vaccinated = true
myDog2.Canine.Domesticated = true
myDog2.Breed = "Cocker Spaniel"
myDog2.Canine.Animal.Age = 5
myDog2.Canine.Animal.SpeciesName = "Canidae"
myDog2.Canine.Animal.Diet = "Pedigree Chum"
Try the example here: https://play.golang.org/p/ifc1Siw3zv-.
Finally, if we provide names in the structs to represent the embedded structs, we HAVE to use those names when setting and getting.
This is illustrated in this last example:
type Canine struct {
AnimalInfo Animal
Domesticated bool
}
type Dog struct {
CanineInfo Canine
Name string
Breed string
Vaccinated bool
}
myDog3 := new(Dog)
// set the properties
myDog3.Name = "Rolo"
myDog3.Vaccinated = true
myDog3.CanineInfo.Domesticated = true
myDog3.Breed = "Cocker Spaniel"
myDog3.CanineInfo.AnimalInfo.Age = 5
myDog3.CanineInfo.AnimalInfo.SpeciesName = "Canidae"
myDog3.CanineInfo.AnimalInfo.Diet = "Pedigree Chum"
You can play with this example on the Go Playground too: https://play.golang.org/p/CpevvONxc-y.
Once again, quite a few ways to do the same thing!
Struct field tags
I’m going to save struct field tags for a later date. Without going into other subjects such as unmarshalling and marshalling data it would be difficult to provide the context to illustrate them effectively here.
We’ve covered a lot already.
Summary
To recap, in this tutorial we looked at the struct composite type and, using some simple examples, we discovered how to initialise - or create instances - of structs; how to modify instance properties; what is exported (public) versus unexported (private); receivers and the difference between pointer receivers and value receivers and finally; composition in the context of struct types and how that differs conceptually from the inheritance commonly used in other programming languages.
Again, we glossed over a couple of things, they’ll be more relevant if we cover in other contexts. We’ll definitely come back to pointers later and also struct tags.
Hopefully, you found this tutorial useful, and you now feel comfortable working with structs in Go.
Please share my newsletter with your friends and colleagues and encourage them to subscribe - I publish one Golang “learning” newsletter a week, and occasionally, another with a few interesting links I’ve found.
Until next time :)