Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 11

Modeling Configurations and Resources with Structs

Recommended resources

Hello! Welcome to your next lesson in our Go journey.

In our last lesson, we covered maps and, importantly, saw how we could create a thread-safe map by combining a map and a sync.RWMutex inside a custom struct. This was our first glimpse into the power of creating our own data types.

Today, we're going to dive deep into that concept. The learning outcome for this lesson is to define custom data structures using structs to model configurations and resources. As a DevOps engineer, you constantly work with structured data—from YAML config files and JSON API responses to the definitions of cloud resources. Structs are Go's fundamental tool for bringing that kind of structured data into your programs in a type-safe and organized way.

We'll cover how to define and initialize structs, and then focus on two practical applications: modeling application configurations and representing resources like those from a cloud provider's API.

What is a Struct?

A struct, short for structure, is a composite data type that groups together zero or more named fields of arbitrary types. If you have experience with Python, you can think of a Go struct (at this stage) as being similar to a simple class that only holds data attributes. It’s a blueprint for creating objects that hold related information.

For example, to represent a server in your infrastructure, you might want to group its hostname, IP address, and status. A struct is perfect for this:

// A Server struct models a single machine in our infrastructure.
type Server struct {
    Hostname string
    IP       string
    Port     int
    IsOnline bool
}

This Server type now acts as a custom blueprint. We can create variables, or instances, of this type, each representing a different server.

To get a foundational understanding of how to declare and create instances of structs, please watch the first few minutes of the following video.

Go Class: 12 Structs, Struct tags & JSON

This video, from the channel Matt KØDVB, provides a clear introduction to what structs are and the most common ways to initialize them.

Watch from the beginning until 05:06. Focus on: The concept of a struct as an aggregate of different types. The syntax for creating an empty struct and initializing it using dot notation. The more common and robust method of using a 'struct literal' with named fields.

Key Initialization Patterns

As you saw in the video, there are a few ways to create a struct, but one is strongly preferred.

  1. The Zero Value: If you declare a struct variable without explicitly initializing it, each of its fields will be set to their respective zero value (e.g., "" for strings, 0 for integers, false for booleans).

    var s1 Server // s1.Hostname is "", s1.IP is "", s1.Port is 0, etc.
    

    This is useful because you get a usable variable immediately without it being nil.

  2. Struct Literal (Recommended): This is the most common and readable way to create a new struct instance. You explicitly name the fields and provide their values.

    s2 := Server{
        Hostname: "api-server-01",
        IP:       "10.0.1.15",
        Port:     8080,
        IsOnline: true,
    }
    

    You can omit fields, and they will be set to their zero value. The order doesn't matter, which makes your code resilient to changes in the struct definition.


Application 1: Modeling Application Configuration

Now for the most direct application to your work: managing configuration. Instead of passing around individual config variables or a map of strings, you can define a single, clean struct that represents your entire application's configuration. This provides type safety and makes your configuration explicit and easy to understand.

Let's look at a professional-grade example.

Streamlining Go App Configuration with Viper and Struct Tags

This blog post from Leapcell demonstrates using a nested struct to manage configuration for a Go application with the popular Viper library. We'll focus only on how the structs are defined.

Read the section 'Defining Configuration Structure'. Pay close attention to how the main AppConfig struct is composed of other structs like ServerConfig and DatabaseConfig. This is a powerful pattern for organizing complex configurations.

The example in that article introduces two crucial concepts:

  1. Nested Structs: You can embed structs within other structs to create a hierarchical data model. This perfectly mirrors the structure of a typical YAML or JSON configuration file.
  2. Struct Tags: The text in backticks, like `mapstructure:"port"`, is a struct tag. This is metadata you can add to a struct's fields. These tags don't change how your Go code works, but they provide instructions to other packages that inspect your structs, a process known as reflection.

The Power of Struct Tags

Struct tags are essential for mapping your Go struct fields to external data representations like JSON, YAML, or database columns. This is because Go has a strict convention: for a field to be visible and accessible by another package (like a JSON encoder), it must be exported, meaning its name must start with a capital letter.

However, JSON and YAML keys are conventionally written in camelCase or snake_case. Struct tags bridge this gap.

type User struct {
    // This field is exported (starts with 'U')
    // The json tag tells the encoding/json package to use 'userId' in the JSON output.
    UserID   int    `json:"userId"`
    FullName string `json:"fullName"`

    // This field is unexported (starts with 's')
    // It will be ignored by the encoding/json package.
    sessionToken string
}

The following video section provides an excellent, in-depth explanation of how struct tags work with JSON, which is directly applicable to YAML and other formats as well.

Go Class: 12 Structs, Struct tags & JSON

Let's return to Matt KØDVB's video. This section details how struct tags are used for JSON serialization, covering field renaming, omitting empty fields, and the critical role of exported fields.

Watch from 30:53 to 40:33. This is a critical section. Focus on: The json:"..." tag syntax. How a tag renames a field in the JSON output (e.g., Page becomes page). The meaning of the omitempty option. What happens when you try to marshal an unexported (lowercase) field.

Test your understanding!

You need to process JSON data from a monitoring alert that looks like this:

{
  "alertName": "HighCPUUsage",
  "instance": "prod-worker-5",
  "severity": "critical",
  "triggeredAt": "2023-10-27T10:00:00Z"
}

Define a Go struct named Alert that can be used to unmarshal this JSON. Use struct tags to map the JSON keys to idiomatic Go field names (e.g., AlertName).

Show answer

Here is the correct struct definition. Note the capitalized field names in Go and the struct tags matching the lowercase JSON keys.

import "time"

type Alert struct {
    AlertName   string    `json:"alertName"`
    Instance    string    `json:"instance"`
    Severity    string    `json:"severity"`
    TriggeredAt time.Time `json:"triggeredAt"` // The encoding/json package can parse this standard time format.
}

Application 2: Modeling API Resources

The same principles apply when you're building a tool to interact with an API. A struct is the perfect way to model the resources you're managing. For instance, if you were writing a tool to manage Kubernetes pods, you might define a struct like this:

// Pod represents a simplified Kubernetes Pod object
type Pod struct {
    Name       string `json:"name"`
    Namespace  string `json:"namespace"`
    Status     string `json:"status"`
    Node       string `json:"nodeName"`
    Containers int    `json:"containerCount"`
}

When you make an API call to the Kubernetes API server and get a JSON response describing a list of pods, you can unmarshal that JSON directly into a slice of these Pod structs ([]Pod). This gives you a structured, type-safe way to work with the data, which is far more reliable than manually navigating a map[string]interface{}.


Conclusion

You've now learned one of the most important building blocks in Go. Structs allow you to create your own types, bringing clarity, organization, and type safety to your programs.

Key Takeaways:

  • Structs group related data into a single, logical unit.
  • They are the idiomatic way to model complex data like application configurations and API resources.
  • Struct tags are essential metadata for mapping Go fields to external data formats like JSON and YAML.
  • Fields must be exported (capitalized) for external packages (like encoding/json) to access them.
  • Nested structs are a powerful tool for representing hierarchical data.

Next Up

We've defined structs to hold our data. The next logical step is to define the behavior associated with that data. In the next lesson, we will learn how to attach functions to our structs. These special functions are called methods, and they are key to encapsulating logic and building well-designed, maintainable Go applications.