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 the
Employee
data type. HenceEmployee
is the struct type whileross
is a structwhile
Thestruct
keyword is a built-in type. If this would in OOP paradigm, we would be callingEmployee
a class andross
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 anEmployee
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 anil
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 thegetSalary
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
ande.LastName
) toe.FullName
function becauseFullName
field belongs to the same structe
, 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.