Exercise - Use structs

Completed

There are times when you need to represent a collection of fields in one structure. For instance, when you need to write a payroll program, you need to use an employee data structure. In Go, you can use structs to group together different fields that could form a record.

A struct in Go is another data type that could contain zero or more fields of arbitrary types and represent them as a single entity.

In this section, we'll explore why structs are essential and how to use them.

Declare and initialize a struct

To declare a struct, you need to use the struct keyword, along with the list of fields and their types that you want your new data type to have. For instance, to define an employee struct, you could use the following code:

type Employee struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

Then, you can declare a variable with the new type as you'd typically do with other types, like this:

var john Employee

And if you want to declare and initialize a variable at the same time, you could do it this way:

employee := Employee{1001, "John", "Doe", "Doe's Street"}

Notice that you have to specify a value for each of the fields from the struct. But that could be problematic sometimes. Alternatively, you can be more specific about the fields you want to initialize in a struct:

employee := Employee{LastName: "Doe", FirstName: "John"}

Notice that from the previous statement, the order you assign values to each field doesn't matter. Also, it doesn't matter if you don't specify a value for any other field. Go will assign a default value depending on the field data type.

To access individual fields of a struct, you can do it by using the dot notation (.), like this example:

employee.ID = 1001
fmt.Println(employee.FirstName)

Lastly, you can use the & operator to yield a pointer to the struct, like the following code demonstrates:

package main

import "fmt"

type Employee struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

func main() {
    employee := Employee{LastName: "Doe", FirstName: "John"}
    fmt.Println(employee)
    employeeCopy := &employee
    employeeCopy.FirstName = "David"
    fmt.Println(employee)
}

When you run the preceding code, you see the following output:

{0 John Doe }
{0 David Doe }

Notice how the struct becomes mutable when you use pointers.

Struct embedding

Structs in Go allow you to embed another struct within a struct. There are going to be times where you want to reduce repetition and reuse a common struct. For example, let's say that you want to refactor the previous code to have a data type for an Employee and another one for a Contractor. You could have a Person struct that holds common fields, like this example:

type Person struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

You can then declare other types that embed a Person type, like an Employee and a Contractor. To embed another struct, you create a new field, like this example:

type Employee struct {
    Information Person
    ManagerID   int
}

But to reference a field from the Person struct, you'll need to include the Information field from an employee variable, like this example:

var employee Employee
employee.Information.FirstName = "John"

If you're refactoring code like we're doing, that would break our code. Alternatively, you can include a new field with the same name of the struct you're embedding, like this example:

type Employee struct {
    Person
    ManagerID int
}

As a demonstration, you could use the following code:

package main

import "fmt"

type Person struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

type Employee struct {
    Person
    ManagerID int
}

type Contractor struct {
    Person
    CompanyID int
}

func main() {
    employee := Employee{
        Person: Person{
            FirstName: "John",
        },
    }
    employee.LastName = "Doe"
    fmt.Println(employee.FirstName)
}

Notice how you access the FirstName field from an Employee struct without having to specify the Person field because it's embedding all its fields automatically. But, when you're initializing a struct, you have to be specific about which field you want to assign a value to.

Encode and decode structs with JSON

Finally, you can use structs to encode and decode data in JSON. Go has excellent support for the JSON format, and it's already included in the standard library packages.

You can also do things like rename the name of a field from the struct. For instance, let's say that you don't want the JSON output to show FirstName but simply name or ignore empty fields. You could use field tags like this example shows:

type Person struct {
    ID        int    
    FirstName string `json:"name"`
    LastName  string
    Address   string `json:"address,omitempty"`
}

Then, to encode a struct into JSON, you use the json.Marshal function. And to decode a JSON string into a data structure, you use the json.Unmarshal function. Here's an example that puts everything together, encoding an array of employees to JSON and decoding the output into a new variable:

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    ID        int
    FirstName string `json:"name"`
    LastName  string
    Address   string `json:"address,omitempty"`
}

type Employee struct {
    Person
    ManagerID int
}

type Contractor struct {
    Person
    CompanyID int
}

func main() {
    employees := []Employee{
        Employee{
            Person: Person{
                LastName: "Doe", FirstName: "John",
            },
        },
        Employee{
            Person: Person{
                LastName: "Campbell", FirstName: "David",
            },
        },
    }

    data, _ := json.Marshal(employees)
    fmt.Printf("%s\n", data)

    var decoded []Employee
    json.Unmarshal(data, &decoded)
    fmt.Printf("%v", decoded)
}

When you run the preceding code, you see the following output:

[{"ID":0,"name":"John","LastName":"Doe","ManagerID":0},{"ID":0,"name":"David","LastName":"Campbell","ManagerID":0}]
[{{0 John Doe } 0} {{0 David Campbell } 0}]