Structures in Go (structs)

Unlike traditional Object-Oriented Programming, Go does not have class-object architecture. Rather, we have structures that hold complex data structures.

What is a struct?

A struct or structure can be compared with the class in the Object-Oriented Programming paradigm. If you don’t know what OOP is, then imagine struct being a recipe that declares the ingredients and the kind of each ingredient.

A structure has different fields of the same or different data types. If you compare structure with a recipe, field names of the structure become the ingredients (like salt) and field types become the kind of these ingredients (like table salt).

A structure is used mainly when you need to define a schema made of different individual fields (properties). Like a class, we can create an object from this schema (class is analogous to the schema).

Since we can instantiate a structure, there should be some nomenclature distinction between the structure and the instance. Hence, the name struct type is used to represent the structure schema and struct or structure is used to represent the instance.

We can say, ross is a type of Employee (struct type) which has firstName, LastName, salary and fullTime properties (structure fields).

Declaring a struct type

A struct type is nothing but a schema containing the blueprint of a data a structure will hold. To make things simple, we need to create a new derived type so that we can refer to the struct type easily. We use struct keyword to create a new structure type as shown in the example below.

type StructName struct {
    field1 fieldType1
    field2 fieldType2
}

In the above syntax, StructName is a struct type while field1 and field2 are fields of data type fieldType1 and fieldType2 respectively.

Let’s create a struct type Employee like we discussed but with some real fields.

type Employee struct {
	firstName string
	lastName string
	salary int
	fullTime bool
}

You can also define different fields of the same data type in the same line as we have seen in Data Types lesson.

type Employee struct {
	firstName, lastName string
	salary int
	fullTime bool
}

Creating a struct

Now that we have a struct type Employee, let’s create a struct ross from it. Since Employee is a type (custom data type), declaring a variable of the type Employee is the same as usual.

package main

import "fmt"

type Employee struct {
	firstName, lastName string
	salary              int
	fullTime            bool
}

func main() {
	var ross Employee
	fmt.Println(ross)
}

// {  0 false}

The output of the above program may look weird to you, but it is giving the zero value of the struct. This happens because we have defined the variable ross of the data type Employee but haven’t initialized it.

The zero value of a struct is a struct with all fields set to their own zero values. Hence string will have zero value of ""(can’t be printed), int will have zero value of 0 and bool will have zero value of false.

When we are saying struct, we are referring to the variable which holds the value of theEmployee data type. Hence Employee is the struct type while ross is a structwhileThe struct keyword is a built-in type. If this would in OOP paradigm, we would be calling Employee a class and ross an object.

Getting and setting struct fields

Getting and setting a struct field is very simple. When a struct variable is created, we can access its fields using . (dot) operator.

In the above program, we’ve created a struct ross that has 4 fields. To assign a value to the field firstName, you need to use the syntax ross.firstName = "ross". Let’s give ross some identity.

package main

import "fmt"

type Employee struct {
	firstName, lastName string
	salary              int
	fullTime            bool
}

func main() {
	var ross Employee
	ross.firstName = "ross"
	ross.lastName = "Bing"
	ross.salary = 1200
	ross.fullTime = true

	fmt.Println("ross.firstName =", ross.firstName)
	fmt.Println("ross.lastName =", ross.lastName)
	fmt.Println("ross.salary =", ross.salary)
	fmt.Println("ross.fullTime =", ross.fullTime)
}

// ross.firstName = ross
// ross.lastName = Bing
// ross.salary = 1200
// ross.fullTime = true

Initializing a struct

Instead of creating an empty struct (just declaring a variable with zero value) and then assigning values to its fields individually, we can create a struct with field values initialized in the same syntax, just like a variable.

package main

import "fmt"

type Employee struct {
	firstName, lastName string
	salary              int
	fullTime            bool
}

func main() {
	ross := Employee{
		firstName: "ross",
		lastName:  "Bing",
		fullTime:  true,
		salary:    1200,
	}

	fmt.Println(ross)
}

// {ross Bing 1200 true}

We have used the shorthand notation (using *:=* syntax) to create the variable ross so that Go can infer type Employee automatically. The order of the appearance of struct’s fields does not matter, as you can see, we have initialized the fullTime field before the salary field.

💡 The comma (,) is absolutely necessary after the value assignment of the last field while creating a struct using the above syntax. This way, Go won’t add a semicolon just after the last field while compiling the code.

You can also initialize only some fields of a struct and leave others to their zero values. In the example below, the value of the struct ross will be {ross Bing 0 true} as his salary is at its zero value of 0.

ross := Employee {
    firstName: "ross",
    lastName:  "Bing",
    fullTime:  true,
}

There is one other way of initializing a struct that does not include field name declarations like below.

ross := Employee{"Ross", "Geller", 1200, true}

The above syntax is perfectly valid. But when creating a struct without declaring the field names, you need to provide all field values in the order of their appearance in the struct type

Anonymous struct

An anonymous struct is a struct with no explicitly defined derived struct type. So far, we have created Employee struct type which ross infers. But in case of an anonymous struct, we do not define any derived struct type and we create a struct by defining the inline struct type and initial values of the struct fields in the same syntax.

package main

import "fmt"

func main() {
	monica := struct {
		firstName, lastName string
		salary              int
		fullTime            bool
	}{
		firstName: "Monica",
		lastName:  "Geller",
		salary:    1200,
	}

	fmt.Println(monica)
}

// {Monica Geller 1200 false}

In the above program, we are creating a struct monica without defining a derived struct type. This is useful when you don’t want to re-use a struct type.

So you would be guessing if ross is of a type of Employee then what is the type of monica here? By using fmt.Printf function and %T format syntax, we get the following result.

fmt.Printf("%T", monica)

// result
struct {firstName string; lastName string; salary int; fullTime bool}

Looks weird? But not at all. Because this is how the Employee would have actually looked like if we wouldn’t have aliased it. Creating a derived type from the built-in struct type gives us the flexibility to reuse it without having to write complex syntax again and again.

Pointer to a struct

Instead of creating a struct, we can create a pointer that points to the value of a struct, in just one statement. This saves one more step to create a struct (variable) and then creating a pointer to that variable (value to the pointer).

The syntax to create a pointer to a struct is as follows.

s := &StructType{...}

Let’s create a pointer ross which points to a struct value.

package main

import "fmt"

type Employee struct {
	firstName, lastName string
	salary              int
	fullTime            bool
}

func main() {
	ross := &Employee{
		firstName: "ross",
		lastName:  "Bing",
		salary:    1200,
		fullTime:  true,
	}

	fmt.Println("firstName", (*ross).firstName)
}

// firstName ross

In the above program, since ross is a pointer, we need to use *ross dereferencing syntax to get the actual value of the struct it is pointing to and the use (*ross).firstName to access firstName of that struct value.

We are using parenthesis around the pointer dereferencing syntax in the program above so that compiler doesn’t get confused between (*ross).firstName and *(ross.firstName).

But Go provide an easy alternative syntax to access fields. We can access the fields of a struct pointer without dereferencing it first. Go will take care of dereferencing a pointer under the hood.

ross := &Employee {
    firstName: "ross",
    lastName:  "Bing",
    salary:    1200,
    fullTime:  true,
}

fmt.Println("firstName", ross.firstName) // ross is a pointer

Anonymous fields

You can define a struct type without declaring any field names. You have to just define the field data types and Go will use the data type declarations (keywords) as the field names.

package main

import "fmt"

type Data struct {
	string
	int
	bool
}

func main() {
	sample1 := Data{"Monday", 1200, true}
	sample1.bool = false

	fmt.Println(sample1.string, sample1.int, sample1.bool)
}

// Monday 1200 false

In the above program, we have defined only the data types on the Data struct type. Go under the hood will use these field types as the name of the fields. There is no difference whatsoever between the struct type defined this way and the struct types we have defined earlier.

Just, in this case, Go helped us creating field names automatically. You are allowed to mix some anonymous fields with named fields like below.

type Employee struct {
	firstName, lastName string
	salary              int
	bool                         // anonymous field
}

Nested struct

A struct field can be of any data type. Hence, it is perfectly legal to have a struct field that holds another struct. Hence, a struct field can have a data type that is a struct type. When a struct field has a struct value, that struct value is called a nested struct since it is nested inside a parent struct.

package main

import "fmt"

type Salary struct {
	basic     int
	insurance int
	allowance int
}

type Employee struct {
	firstName, lastName string
	salary              Salary
	bool
}

func main() {
	ross := Employee{
		firstName: "Ross",
		lastName:  "Geller",
		bool:      true,
		salary:    Salary{1100, 50, 50},
	}
	fmt.Println(ross)
}

// {Ross Geller {1100 50 50} true}

As you can see in the above example, we have created a new struct type Salary that defines an employee’s salary. Then we’ve modified the salary field of the Employee struct type that now holds a value of type Salary.

When creating ross struct of the type Employee, we initialized all fields even the salary field. Since the salary field holds the struct of type Salary, we can assign a struct value to it. We have used the short approach of excluding field names while initializing the salary struct.

Normally, you would access a field of a struct using struct.field syntax, as we have seen before. You can access the salary field in the same manner like ross.salary which returns a struct. Then you can then access (or update) fields of this nested struct using the same approach, like for example, ross.salary.basic. Let’s see this in action.

package main

import "fmt"

type Salary struct {
	basic     int
	insurance int
	allowance int
}

type Employee struct {
	firstName, lastName string
	salary              Salary
	bool
}

func main() {
	ross := Employee{
		firstName: "Ross",
		lastName:  "Geller",
		bool:      true,
		salary:    Salary{1100, 50, 50},
	}
	fmt.Println("Ross's basic salary", ross.salary.basic)
}

// Ross's basic salary 1100

Promoted fields

We have learned that it is perfectly legal to define a struct type without declaring the field names and Go will define the field names from the field types. This approach can also be applied in the nested struct.

We can drop the field name of a nested struct and Go will use struct type as the field name. Let’s see an example.

package main

import "fmt"

type Salary struct {
	basic     int
	insurance int
	allowance int
}

type Employee struct {
	firstName, lastName string
	Salary
}

func main() {
	ross := Employee{
		firstName: "Ross",
		lastName:  "Geller",
		Salary:    Salary{1100, 50, 50},
	}
	fmt.Println("Ross's basic salary", ross.Salary.basic)
}

// Ross's basic salary 1100

Using the previous example of the nested struct, we removed salary field name and just used the Salary struct type to create the anonymous field. Then we can use . (dot notation) approach to get and set the values of the Salary struct fields (nested struct of ross) as usual.

But the cool thing about Go is that, when we use an anonymous nested struct, all the nested struct fields are automatically available on parent struct. This is called field promotion.

package main

import "fmt"

type Salary struct {
	basic     int
	insurance int
	allowance int
}

type Employee struct {
	firstName, lastName string
	Salary
}

func main() {
	ross := Employee{
		firstName: "Ross",
		lastName:  "Geller",
		Salary:    Salary{1100, 50, 50},
	}

	ross.basic = 1200
	ross.insurance = 0
	ross.allowance = 0
	fmt.Println("Ross's basic salary", ross.basic)
	fmt.Println("Ross is", ross)
}

// Ross's basic salary 1200
// Ross is {Ross Geller {1200 0 0}}

In the above program, all fields of the anonymously nested struct Salary has been promoted to parent struct Employee and we can access these fields as if they were defined on the Employee struct.

💡 If a nested anonymous struct has a same field (field name) that conflcts with the field name defined in the parent struct, then that field won’t get promoted. Only the non-conflicting fields will get promoted.

Nested interface

Like a struct, an interface can also be nested in a struct. In Layman’s terms, it means that a field can have a data type of an interface.

💡 Please read the interfaces lesson before you read this section.

Since we know that, an interface type is a declaration of method signatures. Any data type that implements an interface can also be represented as a type of that interface (polymorphism).

Using this polymorphism principle, we can have a struct field of an interface type and its value can be anything that implements that interface. Let’s see this in action using the earlier example.

package main

import "fmt"

type Salaried interface {
	getSalary() int
}

type Salary struct {
	basic     int
	insurance int
	allowance int
}

func (s Salary) getSalary() int {
	return s.basic + s.insurance + s.allowance
}

type Employee struct {
	firstName, lastName string
	salary              Salaried
}

func main() {
	ross := Employee{
		firstName: "Ross",
		lastName:  "Geller",
		salary:    Salary{1100, 50, 50},
	}

	fmt.Println("Ross's  salary is", ross.salary.getSalary())
}

// Ross's  salary is 1200

In the above example, we have created Salaried interface that has getSalary method signature. Since Salary struct implements this method, it implements Salaried interface. Hence, we can store an instance of Salary struct type in a field of Salaried type.

We have done in this on line no. 28 by assigning an instance of Salary struct to the salary field of the Salaried interface.

When we call a method on a variable of an interface type, the method will be executed on the dynamic value that interface represents, which at the moment, is an instance of Salary struct (from line no. 28).

💡 If we did not assign any value to the salary field while creating an Employee struct as we did on line no. 25, the Go will panic with a runtime error as we are trying to call a method on a nil value which is the default dynamic value of an interface. Hence, try to avoid having a struct field of an interface type.

Similar to the field promotions we saw earlier, methods are also promoted when a struct field is an anonymous interface.

package main

import "fmt"

type Salaried interface {
	getSalary() int
}

type Salary struct {
	basic     int
	insurance int
	allowance int
}

func (s Salary) getSalary() int {
	return s.basic + s.insurance + s.allowance
}

type Employee struct {
	firstName, lastName string
	Salaried
}

func main() {
	ross := Employee{
		firstName: "Ross",
		lastName:  "Geller",
		Salaried:  Salary{1100, 50, 50},
	}

	fmt.Println("Ross's salary is", ross.getSalary())
}

// Ross's salary is 1200

As we can see in the above example, we remove the salary field from the Employee struct type and now Salaried is both the field name and the field data type. This is a basic example of an anonymously nested interface.

This will promote all the methods from Salaried interface on the parent struct Employee as if the Employee struct type implements those methods. Hence, we are able to call the getSalary method on the ross which is an instance of Employee struct type (from line no. 31).

💡 Similar to the field promotions of an anonymously nested struct, only the non-conflicting methods will get promoted. Hence, if the Employee struct type also implements the getSalary method, then that will be used instead.

The most important thing to remember here is, in contrast with field promotion as we’ve seen earlier, only the methods are promoted when the anonymous field is an interface. This can be verified by the below example.

package main

import "fmt"

type Salaried interface {
	getSalary() int
}

type Salary struct {
	basic     int
	insurance int
	allowance int
}

func (s Salary) getSalary() int {
	return s.basic + s.insurance + s.allowance
}

type Employee struct {
	firstName, lastName string
	Salaried
}

func main() {
	ross := Employee{
		firstName: "Ross",
		lastName:  "Geller",
		Salaried:  Salary{1100, 50, 50},
	}

	fmt.Println("Ross's basic salary is", ross.basic)
}

// ./prog.go:31:45: ross.basic undefined (type Employee has no field or method basic)

From the above example, we have Salaried as the anonymously nested interface. Since Salary implements that interface, we can assign the value of Salary struct type to the Salaried field. However, this does not mean that Salary is a nested struct, hence its fields won’t get promoted.

Exported fields

As we have seen in the packages lesson, any variable or type that starts with an uppercase letter is exported from that package. In the case of structures, we made sure that all of our structs used in this lesson should be exported, hence they start with an uppercase letter viz. Employee, Salary, Data etc.

But the really cool thing about struct is, we can also control which fields of an exported struct are visible outside the package (or exported). To export the field names of a struct, we’ve to follow the same uppercase letter approach.

type Employee struct {
    FirstName, LastName string
    salary int
    fullTime bool
}

In the above struct type Employee, FirstName and LastName are the only two fields which are exported or visible outside the package.

Let’s create a simple package organization with the package name org. We can create a file WORKSPACE/src/org/employee.go and place the following code inside it. This file exports the Employee struct type.

// employee.go
package org
type Employee struct {
	FirstName, LastName string
	salary              int
	fullTime            bool
}

In the main package, we can import the Employee struct type like below.

// main.go
package main

import (
	"fmt"
	"org"
)

func main() {
    ross := org.Employee{
        FirstName: "Ross",
        LastName:  "Geller",
        salary:    1200
    }

    fmt.Println(ross)
}

The program above won’t compile and the compiler will throw the below error.

unknown field 'salary' in struct literal of type org.Employee

This happens because salary field is not exported from the Employee struct type. We also had to use org.Employee as struct type because of Employee type comes from org package. But we can create a derived type in the main package to make things simpler.

// main.go
package main

import (
	"fmt"
	"org"
)

type Employee org.Employee

func main() {
    ross := Employee{
        FirstName: "Ross",
        LastName:  "Geller",
    }

    fmt.Println(ross)
}

Above program yields below the result.

{Ross Geller 0 false}

Looks weird? Perhaps, because we did not expect the value of the salary and fullTime fields. When we import any struct from another package, we get struct type as it is, just that we don’t have any control over unexported fields. This is useful when you want to protect some fields but still make them useful as the default or constant values or perhaps something complex

What will happen in the case of a nested struct?

  • A nested struct must also be declared with an uppercase letter so that other packages can import it.
  • Nested struct fields starting with an uppercase letter are exported.
  • If a nested struct is anonymous, then its fields starting with an uppercase letter will be available as promoted fields.

Function fields

If you remember discussing function as a type and function as a value from functions lesson, you can pretty much guess that struct fields can also be functions.

So let’s create a simple struct function field which returns the full name of an employee.

package main

import "fmt"

type FullNameType func(string, string) string

type Employee struct {
	FirstName, LastName string
	FullName            FullNameType
}

func main() {
	e := Employee{
		FirstName: "Ross",
		LastName:  "Geller",
		FullName: func(firstName string, lastName string) string {
			return firstName + " " + lastName
		},
	}

	fmt.Println(e.FullName(e.FirstName, e.LastName))
}

// Ross Geller

In the above program, we have defined struct type Employee which has two string fields and one function field. Just for simplicity, we have created a derived function type FullNameType.

While creating the struct e, we need to make sure the field FullName follows the function type syntax. In the above case, we assigned it with an anonymous function. Since the syntax of this anonymous function and the FullNameType declaration matches, this is perfectly legal.

Then we simply executed the function e.FullName with two string arguments e.FirstName and e.LastName.

💡 If you are wondering, why we need to pass properties of e (viz. e.FirstName and e.LastName) to e.FullName function because FullName field belongs to the same struct e, then you need to see the methods lesson.

Struct comparison

Two structs are comparable if they belong to the same type and have the same field values.

package main

import "fmt"

type Employee struct {
	firstName, lastName string
	salary              int
}

func main() {
	ross := Employee{
		firstName: "Ross",
		lastName:  "Geller",
		salary:    1200,
	}

	rossCopy := Employee{
		firstName: "Ross",
		lastName:  "Geller",
		salary:    1200,
	}

	fmt.Println(ross == rossCopy)
}

// true

Above program prints true because both ross and rossCopy belongs to the same struct type Employee and have the same set of field values.

However, if a struct has field type which can be compared, for example, the map which is not comparable, then the struct won’t be comparable.

For example, if Employee struct type has a leaves field of data type map, we could not do the above comparison.

type Employee struct {
	firstName, lastName string
	salary              int
	leaves              map[string]int
}

Struct field meta-data

Struct gives one more ability to add meta-data to its fields. Usually, it is used to provide transformation information on how a struct field is encoded to or decoded from another format (or stored/retrieved from a database), but you can use it to store whatever meta-info you want to, either intended for another package or for your own use.

This meta-information is defined by the string literal (read strings lesson) like below.

type Employee struct {
	firstName string `json:"firstName"`
	lastName  string `json:"lastName"`
	salary    int    `json: "salary"`
	fullTime  int    `json: "fullTime"`
}

In the above example, we are using struct type Employee for JSON encoding/decoding purposes. Read more about JSON encoding/decoding in my “Working with JSON” tutorial.

#golang #structs