9 minutes read

Today we continue our discussion about structs. In this topic, you will learn about advanced struct usage concepts: anonymous structs, nested structs (those that contain another struct as one or many of its fields), structs with anonymous fields, structs with promoted fields, and struct tags in Go.

Anonymous structs

We covered previously a basic struct type declaration by creating a new data type. However, it is also possible to declare a struct without a new data type. These kinds of structs are called anonymous structs. Let's take a look at an example:

package main

import "fmt"

var teslaModelS = struct {
    Brand, Model, Color string
    weightInKg          int
}{
    Brand:      "Tesla",
    Model:      "Model S",
    Color:      "Red",
    weightInKg: 2250,
}

func main() {
    fmt.Println(teslaModelS) // {Tesla Model S Red 2250}
}

Above, we declare the teslaModelS anonymous struct with three steps. First, we apply the var teslaModelS = struct syntax, then declare its fields within curly braces {}, and finally, initialize the fields with a struct literal.

Anonymous structs must be immediately accompanied by a struct literal that initializes its fields. Otherwise, we will get a compiler error indicating that the anonymous struct is not an expression.

Anonymous structs provide a quick way to create a struct without actually creating a new data type in our program. Besides, if we want to use a struct only once in our program, it makes sense to declare it as an anonymous struct, so that we can't accidentally use it again. Apart from that, one of the most common applications of anonymous structs is to program table-driven unit tests.

Nested structs

Since structs allow us to group and combine different data types, a struct may contain one or many structs as part of its fields. To better understand the nested struct concept, let's go ahead and create two structs, namely Address and Employee:

type Address struct {
    City  string
    State string
}

type Employee struct {
    Name    string
    Age     int
    Salary  float64
    Address Address // this field is the nested struct Address within Employee
}

func main() {
    homer := Employee{
        Name:    "Homer",
        Age:     39,
        Salary:  724.38,
        Address: Address{City: "Springfield", State: "Oregon"},
    }

    // we can print the nested struct fields using two instances of the '.' operator:
    fmt.Println("City:", homer.Address.City)   // City: Springfield
    fmt.Println("State:", homer.Address.State) // State: Oregon
}

After declaring the Address field within the Employee struct, we'll be able to access and print the nested struct fields City and State of the Address struct by using the . operator twice.

Anonymous fields

It is possible to create a struct with fields that contain only a data type without the field name. We call these kinds of fields anonymous fields. To learn how they function, we will create a Country struct with two anonymous fields:

type Country struct {
    string
    int
}

Even though the two anonymous fields string and int don't have a defined name, they get the same name as their data type, by default.

To explain this concept further, we are going to assign values to the anonymous fields:

...
var france Country

// this is how we assign values to the anonymous fields,
// the name of the fields is the same as the data type
france.string = "France"
france.int = 67413000

fmt.Println("Country name:", france.string) // Country name: France
fmt.Println("Population:", france.int)      // Population: 67413000

When we create a struct that has anonymous fields, we can only have one anonymous field per data type! For example, if we had two string type anonymous fields in the previously created Country struct, we would get the following compiler errors: duplicate field string and ambiguous selector france.string.

Promoted fields are fields that belong to an anonymous struct field within a struct. We can access promoted fields as if they belong directly to the struct that holds the anonymous struct field.

Since the promoted field concept can be a little complex to understand, let's explore it through an example. We'll start by removing the name of the Address field within our Employee struct: this way, we will declare it as an anonymous field, just passing the Address struct type directly:

...
type Employee struct {
    Name    string
    Age     int
    Salary  float64
    Address // now this field doesn't have a name, just the Address struct type
}

func main() {
    var homer Employee
    homer.Name = "Homer"
    homer.Age = 39
    homer.Salary = 724.38
    
    // we can access the promoted fields of the nested struct Address directly:
    homer.City = "Springfield"
    homer.State = "Oregon"

    // we can print the promoted fields City and State directly:
    fmt.Println("City:", homer.City)   // City: Springfield
    fmt.Println("State:", homer.State) // State: Oregon
}

Since Address is now an anonymous field within the Employee struct, we can access the promoted fields City and State of the Address struct directly, as if they were originally declared in the Employee struct itself.

Struct tags

Go allows us to add struct tags to struct fields. In very simple terms, struct tags are annotations that appear after the type of a field in Go struct declaration.

The most common use of struct tags is to control JavaScript Object Notation (JSON) encoding. To explain the implementation of struct tags in Go, we'll look at an example of formatting and encoding the following JSON response of some random user information:

{
  "userID": "123",
  "isActive": true,
  "lastLogin": "2021-12-08T11:27:32.834056-05:00",
  "userType": 1
}

Now let's take a look at the implementation of the struct tag syntax in the User struct:

type User struct {
                        // this column has the struct tags
    UserID    string    `json:"userID"`
    IsActive  bool      `json:"isActive"`
    LastLogin time.Time `json:"lastLogin"`
    UserType  int       `json:"userType"`
}

Since we are planning to encode a JSON response within the User struct, we need to specify several things to the Go compiler. In particular, we need to indicate the following:

  • struct tags must begin and end with backticks `;

  • they require the json prefix as the struct tag key, followed by the : colon symbol;

  • struct tags need the JSON key within the "" double quotes as the struct tag value.

When working with struct tags, the struct tag key before the : colon symbol usually denotes the encoding package that the subsequent "value" within the double quotes is for. In this particular example, it would be for JSON keys that should be processed by the encoding/json or other third-party packages.

Take notice that apart from JSON struct tags, there are many other commonly used struct tag keys!

Summary

In this topic, we've learned how to deal with structs at an advanced level. We've covered the following questions:

  • How to create anonymous structs and what they are commonly used for.

  • How to create a nested struct and how to initialize and access the data within the nested struct fields.

  • What anonymous fields are and how we can only have one anonymous field per data type in a struct.

  • What promoted fields are and how we can access them directly.

  • How to add struct tags to each of the struct fields, and that we implement struct tags mostly to control JSON encoding in Go.

Now, to make sure you remember all this information and can implement it, let's answer a couple of questions and do some exercises. Good job!

39 learners liked this piece of theory. 2 didn't like it. What about you?
Report a typo