This page looks best with JavaScript enabled

Go structs

 ·  🎃 kr0m

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:


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.

vi 00-structures.go
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)
}
go run 00-structures.go
{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.

vi 01-structures.go
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)
}
go run 01-structures.go
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 string grade, and has its own method studentMethod.
  • teacher: Includes a field of type person, adds the int teaching_years, and has its own method teacherMethod.
vi 02-structures.go
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:

go run 02-structures.go

{{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:

vi 03-structures.go

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:

go run 03-structures.go

{{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:

vi 04-structures.go

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:

go run 04-structures.go

{{ -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.

mkdir structureTest
cd structureTest
go mod init structureTest

The first step is to move the structures to another package and create the methods.

mkdir structures
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.

vi structureTest.go

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.

go run structureTest.go

{{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.

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")
}

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.")
    }
}
vi structureTest.go
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:

go run structureTest.go

{{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.

vi 05-structures.go
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")
    }
}
go run 05-structures.go
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.

vi 06-structures.go
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")
    }
}
go run 06-structures.go
./06-structures.go:20:8: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)
If you liked the article, you can treat me to a RedBull here