Structs in Go

Featured image

At the end of the Functions in Go article, we removed a good amount of duplicate code, but we still had some duplication to print out the myName and myAge variables.

We fixed this duplication in the Functions in Go (challenge answer) article, but our code was still a bit messy for two reasons:

  1. We have to create two new variables for each friend.
  2. Printing how many friends we have is a manual process, instead of letting our programme work it out for us.

We’re going to fix both of these problems in this article, with the help of structs!

What are structs?!

Go’s structs are typed collections of fields. They’re useful for grouping data together to form records. — gobyexample

Let’s jump straight into an example to better understand what this means.

At the end of the Functions in Go (challenge answer) article, we declared our friends, and ourselves, like this:

myName := "Simon"
myAge := 29

friendOneName := "David"
friendOneAge := 17

friendTwoName := "Bill"
friendTwoAge := 42

friendThreeName := "Charlie"
friendThreeAge := 12

friendFourName := "Abby"
friendFourAge := 24

friendFiveName := "Edith"
friendFiveAge := 74

Everyone has a name and an age, so let’s group that data together in a struct. The syntax for declaring a struct is simple:

type person struct {
	name string
	age  int
}

Let’s run through what this example does:

Notice that we’ve used the keyword type in our struct definition. Go is a strongly-typed language, and our new struct is no exception to this rule so person is effectively a new type.

// built-in type
var age int
// our new person type
var me person

In the above example, we’re creating an age variable, of type int, like we’ve done many times before, but we’re also creating variable me, of type person.

Like normal types, there are numerous ways to initialise structs. We’re only going to focus on two:

// Method 1
me := person{"Simon", 28}

// Method 2
me := person{age: 28, name: "Simon"}

For the purposes of this article, we’re going to use the first method. If you’d like to use the second method, feel free! Everything should still work the same.

Now, see if you can convert the following code to use our new person struct, instead of using two separate variables (reach out to us if you get stuck).

myName := "Simon"
myAge := 29

friendOneName := "David"
friendOneAge := 17

friendTwoName := "Bill"
friendTwoAge := 42

friendThreeName := "Charlie"
friendThreeAge := 12

friendFourName := "Abby"
friendFourAge := 24

friendFiveName := "Edith"
friendFiveAge := 74

Done? You should have ended up with something similar to the following:

me := person{"Simon", 29}

friendOne := person{"David", 17}
friendTwo := person{"Bill", 42}
friendThree := person{"Charlie", 12}
friendFour := person{"Abby", 24}
friendFive := person{"Edith", 74}

You’ve probably now got a load of errors showing because you’ve got a load of “undefined” variables. This is because we’re using the old variable names (friendOneName, friendOneAge etc) which no longer exist.

This is the code that is now wrong. But how can we fix it? We no longer have separate variables for name and age

printAgeBracket(myName, myAge, false)

log.Println("I have five friends, and they are as follows:")

printAgeBracket(friendOneName, friendOneAge, true)
printAgeBracket(friendTwoName, friendTwoAge, true)
printAgeBracket(friendThreeName, friendThreeAge, true)
printAgeBracket(friendFourName, friendFourAge, true)
printAgeBracket(friendFiveName, friendFiveAge, true)

With structs, you can access their “properties” using dot notation. That sounds more complicated than it is, let’s see it in action:

package main

import "log"

type person struct {
	name string
	age  int
}

func main() {

	// Start of our code

	me := person{"Simon", 29}

	log.Printf("Hello, my name is %s and I am %d years old", me.name, me.age)

	// End of our code

}

It’s called dot notation because of the dot (.) between the variable name and the property you want to access (e.g. me.name), and it simply means “get me the name property from variable me”.

Can you guess what would be printed?

2019/06/27 17:34:11 Hello, my name is Simon and I am 29 years old

Now, see if you can fix the following code. Hint: you only need to amend these lines of code, nothing else. Remember, you can reach out to us if you get stuck.

printAgeBracket(myName, myAge, false)

log.Println("I have five friends, and they are as follows:")

printAgeBracket(friendOneName, friendOneAge, true)
printAgeBracket(friendTwoName, friendTwoAge, true)
printAgeBracket(friendThreeName, friendThreeAge, true)
printAgeBracket(friendFourName, friendFourAge, true)
printAgeBracket(friendFiveName, friendFiveAge, true)

You should have ended up with the following:

printAgeBracket(me.name, me.age, false)

log.Println("I have five friends, and they are as follows:")

printAgeBracket(friendOne.name, friendOne.age, true)
printAgeBracket(friendTwo.name, friendTwo.age, true)
printAgeBracket(friendThree.name, friendThree.age, true)
printAgeBracket(friendFour.name, friendFour.age, true)
printAgeBracket(friendFive.name, friendFive.age, true)

But wait! Didn’t we say that person was a type? Yes! This means we can change our function to accept a parameter of type person, instead of name and age separately:

func printAgeBracket(p person, isFriend bool) {
	if isFriend {
		log.Printf("%s: ", name)
	} else {
		log.Printf("Hello World. My name is %s.", name)
	}

	if age < 13 {
		log.Println("I am considered a child")
	} else if age < 20 {
		log.Println("I am considered a teenager")
	} else if age < 70 {
		log.Println("I am considered an adult")
	} else {
		log.Println("I am considered a pensioner")
	}
}

But now we have more errors! Let’s see what has gone wrong:

  1. printAgeBracket is referring to undefined variables name and age, because we’re no longer passing them through as parameters.
  2. We’re now passing too many arguments, and arguments of the wrong type, when we call our printAgeBracket function.

See if you can fix these errors before we move on. Reach out to us if you get stuck.

Fixed? Let’s look at what we did.

Our programme now looks like this:

package main

import "log"

type person struct {
	name string
	age  int
}

func printAgeBracket(p person, isFriend bool) {
	if isFriend {
		log.Printf("%s: ", p.name)
	} else {
		log.Printf("Hello World. My name is %s.", p.name)
	}

	if p.age < 13 {
		log.Println("I am considered a child")
	} else if p.age < 20 {
		log.Println("I am considered a teenager")
	} else if p.age < 70 {
		log.Println("I am considered an adult")
	} else {
		log.Println("I am considered a pensioner")
	}
}

func main() {

	// Start of our code

	me := person{"Simon", 29}

	friendOne := person{"David", 17}
	friendTwo := person{"Bill", 42}
	friendThree := person{"Charlie", 12}
	friendFour := person{"Abby", 24}
	friendFive := person{"Edith", 74}

	printAgeBracket(me, false)

	log.Println("I have five friends, and they are as follows:")

	printAgeBracket(friendOne, true)
	printAgeBracket(friendTwo, true)
	printAgeBracket(friendThree, true)
	printAgeBracket(friendFour, true)
	printAgeBracket(friendFive, true)

	// End of our code

}

So, we’re done? No, let’s remind ourselves of the two problems we said we’d fix in this article:

  1. We have to create two new variables for each friend.
  2. Printing how many friends we have was a manual process, instead of letting our programme work it out for us.

We’ve fixed the first issue, but we’re still having to print the number of friends we have manually.

But how can we count the number of friends we have, programmatically? Well, the answer may surprise you. We’re going to use an array.

We’ve already seen how arrays can improve maintainability, compared to separate variables, in the Arrays and Loops in Go article, and we’ve already said that person is a type, so we can create an array of type person.

Let’s remind ourselves how to create a simple array of int’s:

arr := [3]int{1,2,3}

So creating an array of type person is as simple as:

arr := [0]person{}

Or to create an array of person, with 5 values initialised:

friends := [5]person{
	{"David", 17},
	{"Bill", 42},
	{"Charlie", 12},
	{"Abby", 24},
	{"Edith", 74},
}

Let’s now update our code to use this method, instead of creating a separate variable for each friend:

func main() {

	// Start of our code

	me := person{"Simon", 29}

	friends := [5]person{
		{"David", 17},
		{"Bill", 42},
		{"Charlie", 12},
		{"Abby", 24},
		{"Edith", 74},
	}

	printAgeBracket(me, false)

	log.Println("I have five friends, and they are as follows:")

	printAgeBracket(friendOne, true)
	printAgeBracket(friendTwo, true)
	printAgeBracket(friendThree, true)
	printAgeBracket(friendFour, true)
	printAgeBracket(friendFive, true)

	// End of our code

}

We’ve now got more undefined errors because our individual friend variables (friendOne, friendTwo etc) no longer exist.

So now what do we do?! Well, as we know from the Arrays and Loops in Go article, we can loop over the elements in an array.

So, let’s use the same method to loop through our friends array and call our printAgeBracket function:

func main() {

	// Start of our code

	me := person{"Simon", 29}

	friends := [5]person{
		{"David", 17},
		{"Bill", 42},
		{"Charlie", 12},
		{"Abby", 24},
		{"Edith", 74},
	}

	printAgeBracket(me, false)

	log.Println("I have five friends, and they are as follows:")

	for i := 0; i < len(friends); i++ {
		printAgeBracket(friends[i], true)
	}

	// End of our code

}

Now that looks much cleaner! We can also add more friends to our programme, by adding new elements to our friends array. Go ahead, try it!

This is great, but we are still hard-coding our friend count. Let’s fix that now.

We’re already using the len method, which we know gives us the length of an array, so it’s as easy as changing our Println statement to a Printf statement and using the array length:

log.Printf("I have %d friends, and they are as follows:", len(friends))

Yes - it really is as easy as that! Take some time to play with the code, and make sure you understand what we’ve done.

For reference, our programme now looks like this:

package main

import "log"

type person struct {
	name string
	age  int
}

func printAgeBracket(p person, isFriend bool) {
	if isFriend {
		log.Printf("%s: ", p.name)
	} else {
		log.Printf("Hello World. My name is %s.", p.name)
	}

	if p.age < 13 {
		log.Println("I am considered a child")
	} else if p.age < 20 {
		log.Println("I am considered a teenager")
	} else if p.age < 70 {
		log.Println("I am considered an adult")
	} else {
		log.Println("I am considered a pensioner")
	}
}

func main() {

	// Start of our code

	me := person{"Simon", 29}

	friends := [5]person{
		{"David", 17},
		{"Bill", 42},
		{"Charlie", 12},
		{"Abby", 24},
		{"Edith", 74},
	}

	printAgeBracket(me, false)

	log.Printf("I have %d friends, and they are as follows:", len(friends))

	for i := 0; i < len(friends); i++ {
		printAgeBracket(friends[i], true)
	}

	// End of our code

}

Challenge

We’ve come a long way, and our programme looks a lot cleaner, but there’s still more we can do to clean it up.

For example, we are still making separate variables for the me variable and our friends. It would be really good if we didn’t have to do this.

  1. Make a new field in our person struct, of type bool, that we’ll use to control whether someone is a friend or not.
  2. Use the new field to control which statement is printed out, instead of the isFriend parameter.

If anything in this article doesn’t make sense, you need a hint to the challenge, or you just want to show us your solution reach out to us - we’d love to hear from you!