Go structures provide a way to group related data under a single type. Unlike traditional object-oriented languages, Go doesn’t have classes, but structures offer similar functionality by allowing the creation of complex types that can contain multiple fields of different types.
This article is quite extensive, so I’ve organized it as follows:
- Basic Structure.
- Anonymous Structures.
- Methods Associated with Structures.
- Composition.
- Encapsulation of Structures.
- Comparing Structures.
If you’re new to Go, I recommend the following previous articles:
Basic Structure:
Imagine that we want to manage people in our program, and the data about people consists of several pieces of information, such as name, age, and gender. We could create a map for age and another for gender, both using the name as the key, but a structure is much simpler.
In the following example, we create two person
objects with different data for each.
package main
import "fmt"
type person struct {
name string
age int
sex string
}
func main() {
person1 := person {
name: "John",
age: 25,
sex: "Male",
}
person2 := person {
name: "Sarah",
age: 45,
sex: "Female",
}
fmt.Println(person1)
fmt.Println("Name: ", person1.name)
fmt.Println("Age: ", person1.age)
fmt.Println("Sex: ", person1.sex)
fmt.Println(person2)
fmt.Println("Name: ", person2.name)
fmt.Println("Age: ", person2.age)
fmt.Println("Sex: ", person2.sex)
}
{John 25 Male}
Name: John
Age: 25
Sex: Male
{Sarah 45 Female}
Name: Sarah
Age: 45
Sex: Female
Anonymous Structures:
When a structure incorporates another, it is not necessary to define the field as structure2
type in structure1
. These types of fields are called anonymous.
Let’s first see an example where the type is defined.
package main
import (
"fmt"
)
func main() {
type personalData struct {
name string
age int
sex string
}
type client struct {
person personalData
address string
telephone int
}
client1 := client{
person: personalData{
name: "John",
age: 25,
sex: "Male",
},
address: "Paper street",
telephone: 666334455,
}
fmt.Println("client1 name: ", client1.person.name)
fmt.Println("client1 age: ", client1.person.age)
fmt.Println("client1 sex: ", client1.person.sex)
fmt.Println("client1 address: ", client1.address)
fmt.Println("client1 telephone: ", client1.telephone)
}
client1 name: John
client1 age: 25
client1 sex: Male
client1 address: Paper street
client1 telephone: 666334455
The version with anonymous fields would look like this:
package main
import (
"fmt"
)
func main() {
type personalData struct {
name string
age int
sex string
}
type client struct {
// Anonymous field
personalData
address string
telephone int
}
personalDataClient1 := personalData{
name: "John",
age: 25,
sex: "Male",
}
client1 := client{
// Set structure values omitting field names
personalDataClient1,
"Paper street",
666334455,
}
// Direct personalData fields access using client1
fmt.Println("client1 name: ", client1.name)
fmt.Println("client1 age: ", client1.age)
fmt.Println("client1 sex: ", client1.sex)
fmt.Println("client1 address: ", client1.address)
fmt.Println("client1 telephone: ", client1.telephone)
}
client1 name: John
client1 age: 25
client1 sex: Male
client1 address: Paper street
client1 telephone: 666334455
It is even possible to create structures without creating a type beforehand, being able to create them “on the fly” and for single use.
package main
import (
"fmt"
)
func main() {
client1 := struct {
name string
age int
sex string
address string
telephone int
}{
name: "John",
age: 25,
sex: "Male",
address: "Paper street",
telephone: 666334455,
}
fmt.Println("client1 name: ", client1.name)
fmt.Println("client1 age: ", client1.age)
fmt.Println("client1 sex: ", client1.sex)
fmt.Println("client1 address: ", client1.address)
fmt.Println("client1 telephone: ", client1.telephone)
}
client1 name: John
client1 age: 25
client1 sex: Male
client1 address: Paper street
client1 telephone: 666334455
The only limitation of anonymous fields is that they cannot be a slice.
package main
import (
"fmt"
)
func main() {
type client struct {
name string
address string
telephone int
}
type clientList struct {
// Anonymous slice Not allowed
[]client
}
client1 := client{
name: "John",
address: "Paper street",
telephone: 666334455,
}
client2 := client{
name: "Sarah",
address: "Inexistent avenue",
telephone: 555667788,
}
var clientSlice []client
clientSlice = append(clientSlice, client1, client2)
clientList1 := clientList{
clientSlice,
}
for _, v := range clientList1 {
fmt.Println(v.name)
fmt.Println(v.address)
fmt.Println(v.telephone)
}
}
./test.go:15:9: syntax error: unexpected [, expected field name or embedded type
To make it work, we must stop using anonymous fields.
package main
import (
"fmt"
)
func main() {
type client struct {
name string
address string
telephone int
}
type clientList struct {
clients []client
}
client1 := client{
name: "John",
address: "Paper street",
telephone: 666334455,
}
client2 := client{
name: "Sarah",
address: "Inexistent avenue",
telephone: 555667788,
}
var clientSlice []client
clientSlice = append(clientSlice, client1, client2)
clientList1 := clientList{
clientSlice,
}
for _, v := range clientList1.clients {
fmt.Println(v.name)
fmt.Println(v.address)
fmt.Println(v.telephone)
}
}
John
Paper street
666334455
Sarah
Inexistent avenue
555667788
Methods Associated with Structures:
How to link methods to a type was already explained in a previous article on functions , but it never hurts to refresh your memory.
In this example, we will link a structure type to the area()
method. In this method, we get a copy of the object itself. Go calls this a receiver
; in some programming languages, it is called this
. The idiomatic way to name this receiver is to use the first letter of the object type.
package main
import "fmt"
type rectangle struct {
Base int
Height int
}
func (r rectangle) area() int {
return r.Base * r.Height
}
func main() {
// Assign structure variable values
rect := rectangle{Base: 3, Height: 4}
// rect structure moreover has area method available
// Call structure method
area := rect.area()
fmt.Println(area)
}
12
Methods offer several advantages over using functions:
- Methods are logically bound to types.
- Method names can be reused across different types.
Composition:
In Go, composition refers to the ability to embed one type into another, inheriting its behaviors, extending the functionality of an existing type by incorporating other types that provide additional functionalities, and offering greater simplicity and clarity in design by allowing for a more modular and easy-to-understand structure.
In this simple example, we have three structures:
- person: A common structure with a
personMethod
. - student: Includes a field of type
person
, adds the stringgrade
, and has its own methodstudentMethod
. - teacher: Includes a field of type
person
, adds the intteaching_years
, and has its own methodteacherMethod
.
package main
import "fmt"
type person struct {
name string
age int
sex string
}
func (p person) personMethod() {
fmt.Println("personMethod accessed")
}
type student struct {
personData person
grade string
}
func (s student) studentMethod() {
fmt.Println("studentMethod accessed")
}
type teacher struct {
personData person
teachingYears int
}
func (t teacher) teacherMethod() {
fmt.Println("teacherMethod accessed")
}
func main() {
student := student {
personData: person {
name: "Ana",
age: 20,
sex: "Female",
},
grade: "engineering",
}
teacher := teacher {
personData: person {
name: "Joe",
age: 30,
sex: "Male",
},
teachingYears: 10,
}
fmt.Println(student)
fmt.Println("Name: ", student.personData.name)
fmt.Println("Age: ", student.personData.age)
fmt.Println("Sex: ", student.personData.sex)
fmt.Println("Grade: ", student.grade)
student.personData.personMethod()
student.studentMethod()
fmt.Println("-----")
fmt.Println(teacher)
fmt.Println("Name: ", teacher.personData.name)
fmt.Println("Age: ", teacher.personData.age)
fmt.Println("Sex: ", teacher.personData.sex)
fmt.Println("TeachingYears: ", teacher.teachingYears)
teacher.personData.personMethod()
teacher.teacherMethod()
}
Running the code:
{{Ana 20 Female} engineering}
Name: Ana
Age: 20
Sex: Female
Grade: engineering
personMethod accessed
studentMethod accessed
-----
{{Joe 30 Male} 10}
Name: Joe
Age: 30
Sex: Male
TeachingYears: 10
personMethod accessed
teacherMethod accessed
Go structures support anonymous fields, which means we can define fields in structures by type only, without needing to specify a name. This way, the type itself becomes embedded. Using anonymous fields when accessing the structure’s fields or associated methods results in cleaner, simpler code.
The previous example using anonymous fields would look like this:
package main
import "fmt"
type person struct {
name string
age int
sex string
}
func (p person) personMethod() {
fmt.Println("personMethod accessed")
}
type student struct {
person
grade string
}
func (s student) studentMethod() {
fmt.Println("studentMethod accessed")
}
type teacher struct {
person
teachingYears int
}
func (t teacher) teacherMethod() {
fmt.Println("teacherMethod accessed")
}
func main() {
student := student {
person: person {
name: "Ana",
age: 20,
sex: "Female",
},
grade: "engineering",
}
teacher := teacher {
person: person {
name: "Joe",
age: 30,
sex: "Male",
},
teachingYears: 10,
}
fmt.Println(student)
fmt.Println("Name: ", student.name)
fmt.Println("Age: ", student.age)
fmt.Println("Sex: ", student.sex)
fmt.Println("Grade: ", student.grade)
student.personMethod()
student.studentMethod()
fmt.Println("-----")
fmt.Println(teacher)
fmt.Println("Name: ", teacher.name)
fmt.Println("Age: ", teacher.age)
fmt.Println("Sex: ", teacher.sex)
fmt.Println("TeachingYears: ", teacher.teachingYears)
teacher.personMethod()
teacher.teacherMethod()
}
Running the code:
{{Ana 20 Female} engineering}
Name: Ana
Age: 20
Sex: Female
Grade: engineering
personMethod accessed
studentMethod
accessed
-----
{{Joe 30 Male} 10}
Name: Joe
Age: 30
Sex: Male
TeachingYears: 10
personMethod accessed
teacherMethod accessed
Encapsulation of Structures:
There are times when we need to ensure that the fields of a structure have assigned values. In the previous example, we might want to ensure that a person always has a name or that their age is not negative, for example.
One way to ensure that an object of a type meets the required data is to use encapsulation. This involves forcing the instantiation of the type through specific methods.
With the current code, we could instantiate a person without a name:
package main
import "fmt"
type person struct {
name string
age int
sex string
}
func (p person) personMethod() {
fmt.Println("personMethod accessed")
}
type student struct {
person
grade string
}
func (s student) studentMethod() {
fmt.Println("studentMethod accessed")
}
type teacher struct {
person
teachingYears int
}
func (t teacher) teacherMethod() {
fmt.Println("teacherMethod accessed")
}
func main() {
student := student {
person: person {
age: -20,
sex: "Female",
},
grade: "engineering",
}
teacher := teacher {
person: person {
age: -30,
sex: "Male",
},
teachingYears: 10,
}
fmt.Println(student)
fmt.Println("Name: ", student.name)
fmt.Println("Age: ", student.age)
fmt.Println("Sex: ", student.sex)
fmt.Println("Grade: ", student.grade)
student.personMethod()
student.studentMethod()
fmt.Println("-----")
fmt.Println(teacher)
fmt.Println("Name: ", teacher.name)
fmt.Println("Age: ", teacher.age)
fmt.Println("Sex: ", teacher.sex)
fmt.Println("TeachingYears: ", teacher.teachingYears)
teacher.personMethod()
teacher.teacherMethod()
}
This results in the Name field being empty:
{{ -20 Female} engineering}
Name:
Age: -20
Sex: Female
Grade: engineering
personMethod accessed
studentMethod accessed
-----
{{ -30 Male} 10}
Name:
Age: -30
Sex: Male
TeachingYears: 10
personMethod accessed
teacherMethod accessed
With encapsulation, we can solve this problem. The first step is to make the structures inaccessible directly, which is done by moving them to another package. When a type, variable, or function is in another package and their names start with lowercase letters, those types, variables, or functions will not be exposed to other packages.
The idea of encapsulation is to move the structures to another package and create methods that access them, where these methods will be exported because their names will start with uppercase letters, making them visible from other packages.
As a final detail, we must remember that the names of the fields and methods must also start with uppercase if we want to access those fields or methods; otherwise, we will have a structure that we cannot read data from or execute methods on.
To make the module follow the structure of a Go project, let’s initialize a module.
cd structureTest
go mod init structureTest
The first step is to move the structures to another package and create the methods.
vi structures/structures.go
package structures
import "fmt"
type person struct {
Name string
Age int
Sex string
}
func (p person) PersonMethod() {
fmt.Println("personMethod accessed")
}
type student struct {
person
Grade string
}
func (s student) StudentMethod() {
fmt.Println("studentMethod accessed")
}
// New code, uppercase to make it available from outside the current package
func NewStudent(name string, age int, sex, grade string) (student, error) {
newStudent := student{}
if len(name) > 0 && age > 18 && age < 120 && (sex == "Male" || "Female") && len(grade) > 0 {
newStudent = student {
person: person{
Name: name,
Age: age,
Sex: sex,
},
Grade: grade,
}
return newStudent, nil
} else {
return newStudent, fmt.Errorf("Error: Incorrect student data input.")
}
}
type teacher struct {
person
TeachingYears int
}
func (t teacher) TeacherMethod() {
fmt.Println("teacherMethod accessed")
}
// New code, uppercase to make it available from outside the current package
func NewTeacher(name string, age int, sex string, teachingYears int) (teacher, error) {
newTeacher := teacher{}
if len(name) > 0 && age > 18 && age < 120 && (sex == "Male" || "Female") && teachingYears > 0 {
newTeacher = teacher {
person: person{
Name: name,
Age: age,
Sex: sex,
},
TeachingYears: teachingYears,
}
return newTeacher, nil
} else {
return newTeacher, fmt.Errorf("Error: Incorrect teacher data input.")
}
}
Now, in the main file, we need to import the package and call the method to instantiate the student and teacher.
package main
import (
"fmt"
"structureTest/structures"
)
func main() {
student, err := structures.NewStudent("Ana", 20, "Female", "engineering")
if err != nil {
fmt.Println("Error obtaining structures.NewStudent:", err)
return
}
teacher, err := structures.NewTeacher("Joe", 30, "Male", 10)
if err != nil {
fmt.Println("Error obtaining structures.NewTeacher:", err)
return
}
fmt.Println(student)
fmt.Println("Name: ", student.Name)
fmt.Println("Age: ", student.Age)
fmt.Println("Sex: ", student.Sex)
fmt.Println("Grade: ", student.Grade)
student.PersonMethod()
student.StudentMethod()
fmt.Println("-----")
fmt.Println(teacher)
fmt.Println("Name: ", teacher.Name)
fmt.Println("Age: ", teacher.Age)
fmt.Println("Sex: ", teacher.Sex)
fmt.Println("TeachingYears: ", teacher.TeachingYears)
teacher.PersonMethod()
teacher.TeacherMethod()
fmt.Println("-----")
student2, err := structures.NewStudent("", 20, "Female", "engineering")
if err != nil {
fmt.Println("Error obtaining structures.NewStudent:", err)
return
}
fmt.Println(student2)
}
Running the code, we can see that it works without issues, even preventing the creation of a student with an empty name.
{{Ana 20 Female} engineering}
Name: Ana
Age: 20
Sex: Female
Grade: engineering
personMethod accessed
studentMethod accessed
-----
{{Joe 30 Male} 10}
Name: Joe
Age: 30
Sex: Male
TeachingYears: 10
personMethod accessed
teacherMethod accessed
-----
Error obtaining structures.NewStudent: Error: Incorrect student data input.
Encapsulation is not limited to the instantiation of objects; it can also be used to read (getters) or write (setters) the fields of a structure, allowing us to program a fully customized logic for reading and writing fields.
For this, the structure’s fields must be lowercase to make them private, and we need to write methods to read/set values. The idiomatic names for getters/setters are the field name for getters and SetFIELDNAME for setters.
package structures
import "fmt"
type person struct {
name string
age int
sex string
}
func (p person) PersonMethod() {
fmt.Println("personMethod accessed")
}
type student struct {
person
grade string
}
func (s student) StudentMethod() {
fmt.Println("studentMethod accessed")
}
func (s student) Name() string {
return s.name
}
func (s student) Age() int {
return s.age
}
func (s student) Sex() string {
return s.sex
}
func (s student) Grade() string {
return s.grade
}
func (s *student) SetName(newName string) {
s.name = newName
}
func (s *student) SetAge(newAge int) {
s.age = newAge
}
func (s *student) SetSex(newSex string) {
s.sex = newSex
}
func (s *student) SetGrade(newGrade string) {
s.grade = newGrade
}
func NewStudent(name string, age int, sex, grade string) (student, error) {
newStudent := student
{}
if len(name) > 0 && age > 18 && age < 120 && (sex == "Male" || sex == "Female") && len(grade) > 0 {
newStudent = student{
person: person{
name: name,
age: age,
sex: sex,
},
grade: grade,
}
return newStudent, nil
} else {
return newStudent, fmt.Errorf("Error: Incorrect student data input.")
}
}
type teacher struct {
person
teachingYears int
}
func (t teacher) TeacherMethod() {
fmt.Println("teacherMethod accessed")
}
func (t teacher) Name() string {
return t.name
}
func (t teacher) Age() int {
return t.age
}
func (t teacher) Sex() string {
return t.sex
}
func (t teacher) TeachingYears() int {
return t.teachingYears
}
func (t *teacher) SetName(newName string) {
t.name = newName
}
func (t *teacher) SetAge(newAge int) {
t.age = newAge
}
func (t *teacher) SetSex(newSex string) {
t.sex = newSex
}
func (t *teacher) SetTeachingYears(newTeachingYears int) {
t.teachingYears = newTeachingYears
}
func NewTeacher(name string, age int, sex string, teachingYears int) (teacher, error) {
newTeacher := teacher{}
if len(name) > 0 && age > 18 && age < 120 && (sex == "Male" || sex == "Female") && teachingYears > 0 {
newTeacher = teacher{
person: person{
name: name,
age: age,
sex: sex,
},
teachingYears: teachingYears,
}
return newTeacher, nil
} else {
return newTeacher, fmt.Errorf("Error: Incorrect teacher data input.")
}
}
package main
import (
"fmt"
"structureTest/structures"
)
func main() {
student, err := structures.NewStudent("Ana", 20, "Female", "engineering")
if err != nil {
fmt.Println("Error obtaining structures.NewStudent:", err)
return
}
teacher, err := structures.NewTeacher("Joe", 30, "Male", 10)
if err != nil {
fmt.Println("Error obtaining structures.NewTeacher:", err)
return
}
fmt.Println(student)
fmt.Println("Name: ", student.Name())
fmt.Println("Age: ", student.Age())
fmt.Println("Sex: ", student.Sex())
fmt.Println("Grade: ", student.Grade())
student.PersonMethod()
student.StudentMethod()
fmt.Println("-----")
fmt.Println(teacher)
fmt.Println("Name: ", teacher.Name())
fmt.Println("Age: ", teacher.Age())
fmt.Println("Sex: ", teacher.Sex())
fmt.Println("Grade: ", teacher.TeachingYears())
teacher.PersonMethod()
teacher.TeacherMethod()
fmt.Println("--- Resetting student --")
student.SetName("Kevin Mitnick")
student.SetAge(66)
student.SetSex("Male")
student.SetGrade("Hacker")
fmt.Println(student)
fmt.Println("Name: ", student.Name())
fmt.Println("Age: ", student.Age())
fmt.Println("Sex: ", student.Sex())
fmt.Println("Grade: ", student.Grade())
student.PersonMethod()
student.StudentMethod()
fmt.Println("--- Resetting teacher --")
teacher.SetName("Richard Stallman")
teacher.SetAge(99)
teacher.SetSex("Male")
teacher.SetTeachingYears(20)
fmt.Println(teacher)
fmt.Println("Name: ", teacher.Name())
fmt.Println("Age: ", teacher.Age())
fmt.Println("Sex: ", teacher.Sex())
fmt.Println("Grade: ", teacher.TeachingYears())
teacher.PersonMethod()
teacher.TeacherMethod()
}
Running the code, we can see that it continues to work, but this time the values are accessed through the getters, and the values are changed through the setters:
{{Ana 20 Female} engineering}
Name: Ana
Age: 20
Sex: Female
Grade: engineering
personMethod accessed
studentMethod accessed
-----
{{Joe 30 Male} 10}
Name: Joe
Age: 30
Sex: Male
Grade: 10
personMethod accessed
teacherMethod accessed
--- Resetting student --
{{Kevin Mitnick 66 Male} Hacker}
Name: Kevin Mitnick
Age: 66
Sex: Male
Grade: Hacker
personMethod accessed
studentMethod accessed
--- Resetting teacher --
{{Richard Stallman 99 Male} 20}
Name: Richard Stallman
Age: 99
Sex: Male
Grade: 20
personMethod accessed
teacherMethod accessed
Comparing Structures:
Structures can be compared as long as they have the same number and type of fields.
package main
import (
"fmt"
)
type name struct {
firstName string
lastName string
}
func main() {
name1 := name{
firstName: "Steve",
lastName: "Jobs",
}
name2 := name{
firstName: "Steve",
lastName: "Jobs",
}
if name1 == name2 {
fmt.Println("name1 and name2 are equal")
} else {
fmt.Println("name1 and name2 are not equal")
}
name3 := name{
firstName: "Steve",
lastName: "Jobs",
}
name4 := name{
firstName: "Steve",
}
if name3 == name4 {
fmt.Println("name3 and name4 are equal")
} else {
fmt.Println("name3 and name4 are not equal")
}
}
name1 and name2 are equal
name3 and name4 are not equal
But if a field is not comparable, such as maps
, which cannot be compared, we will get an error.
package main
import (
"fmt"
)
type image struct {
data map[int]int
}
func main() {
image1 := image{
data: map[int]int{
0: 155,
}}
image2 := image{
data: map[int]int{
0: 155,
}}
if image1 == image2 {
fmt.Println("image1 and image2 are equal")
}
}
./06-structures.go:20:8: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)