Structs and Data Modeling

Updated

October 31, 2026

Structs and Data Modeling

Overview

Structs are Go’s primary mechanism for creating custom composite types. They group related fields together.

Defining Structs

type Person struct {
    Name    string
    Age     int
    Email   string
    Active  bool
}

Creating Instances

// Zero value
var p Person  // All fields get zero values

// Literal (named fields - preferred)
p := Person{
    Name:  "Alice",
    Age:   30,
    Email: "alice@example.com",
}

// Literal (positional - fragile)
p := Person{"Alice", 30, "alice@example.com", true}

// Using new
p := new(Person)  // *Person with zero values

// Expression-based new (Go 1.26+)
p := new(Person{Name: "Alice"})  // *Person initialized with values
Embedded fields (composition)

Employee
|-- Person
|    |-- Name
|    |-- Age
|-- Address
|    |-- City
|-- Title

Accessing Fields

p.Name = "Bob"
fmt.Println(p.Age)

// Pointer access (automatic dereference)
ptr := &p
ptr.Name = "Carol"  // Same as (*ptr).Name

Anonymous Structs

point := struct {
    X, Y int
}{10, 20}

// Useful for one-off data
data := struct {
    ID    int
    Value string
}{1, "test"}

Embedding (Composition)

Go favors composition over inheritance. When a struct embeds another type anonymously (without an explicit field name), the embedded type’s fields and methods are promoted to the outer struct:

type Address struct {
    City  string
    State string
}

type Contact struct {
    Email string
    Phone string
}

type Employee struct {
    Name string
    Age  int

    Address // anonymous embedding
    Contact // anonymous embedding
}

Because Address and Contact are embedded anonymously, their fields elevate to Employee:

e := Employee{...}

// Promoted field access (plain dot notation)
fmt.Println(e.City)  // same as e.Address.City
fmt.Println(e.Email) // same as e.Contact.Email

Flattened Struct Literals (Go 1.27+)

For sixteen years (since Go 1.0), field promotion had an annoying asymmetry: once an instance existed, you could read its promoted fields with clean dot notation (e.City), yet constructing that same struct required wrapping every embedded type in a “Russian nesting doll”:

// Prior to Go 1.27: the nesting-doll tax
e := Employee{
    Name: "Alice",
    Age:  30,
    Address: Address{
        City:  "NYC",
        State: "NY",
    },
    Contact: Contact{
        Email: "alice@example.com",
        Phone: "555-0100",
    },
}

This structural wrapping created visual friction and rightward drift, especially in deeply nested configurations, data transfer objects (DTOs), and test fixtures.

Originating in proposal #9859 (first opened in 2015), Go 1.27 flattens struct literal construction. Any unambiguously promoted field can now serve directly as a key in composite literals:

// Go 1.27+: flattened composite literal
e := Employee{
    Name:  "Bob",
    Age:   25,
    City:  "LA",              // promoted from Address
    State: "CA",              // promoted from Address
    Email: "bob@example.com", // promoted from Contact
    Phone: "555-0200",        // promoted from Contact
}

This reduces boilerplate by over 30%, eliminates redundant type wrappers, and brings symmetry to Go’s mental model: if you read a field with e.City, you can initialize it with City: "LA".

Multi-Level Flattening

Flattening is not limited to a single level of embedding. Any field that elevates unambiguously under Go’s promotion rules can serve as an initialization key across multiple layers:

type Coordinates struct {
    Latitude  float64
    Longitude float64
}

type Location struct {
    Coordinates // depth 1
    Altitude    float64
}

type Site struct {
    Name     string
    Location // depth 2
}

Prior to Go 1.27, initializing Site required three layers of braces:

// Legacy: 3 layers of nested boilerplate (14 lines)
s := Site{
    Name: "Data Center Alpha",
    Location: Location{
        Coordinates: Coordinates{
            Latitude:  40.7128,
            Longitude: -74.0060,
        },
        Altitude: 10.5,
    },
}

In Go 1.27+, all intermediate wrappers vanish into flat key-value pairs:

// Go 1.27+: multi-level flattened literal (6 lines, 57% reduction)
s := Site{
    Name:      "Data Center Alpha",
    Latitude:  40.7128,
    Longitude: -74.0060,
    Altitude:  10.5,
}

Configuration Structs and Information Density

The ergonomic benefit is most pronounced in nested configuration structs, where structural wrappers often obscure actual values:

type DatabaseConfig struct {
    Host     string
    Port     int
    Name     string
    SSL      bool
    MaxConns int
}

type CacheConfig struct {
    TTL     int
    MaxSize int
}

type AppConfig struct {
    DatabaseConfig
    CacheConfig
}

// Go 1.27: clear, high-density configuration
cfg := AppConfig{
    Host:     "localhost",
    Port:     5432,
    Name:     "app_db",
    SSL:      true,
    MaxConns: 100,
    TTL:      300,
    MaxSize:  256,
}

Rather than reading structural plumbing (DatabaseConfig: DatabaseConfig{...}), readers immediately see the actionable values (Host, Port, TTL).

Ambiguity: When the Compiler Refuses to Guess

Flattened initialization follows Go’s existing field promotion collision rules. If two embedded structs define a field with the exact same name at the same embedding depth, the field is not promoted:

type DatabaseConfig struct {
    Host string
    Port int
}

type CacheConfig struct {
    Host string
    TTL  int
}

type AppConfig struct {
    DatabaseConfig
    CacheConfig
}

Attempting to initialize the ambiguous field using the flat syntax:

cfg := AppConfig{
    Host: "localhost", // compile error!
}

Fails at compile time with a clear diagnostic:

unknown field Host in struct literal of type AppConfig

Because Host exists in both DatabaseConfig and CacheConfig at depth 1, the compiler refuses to guess which embedded struct should receive the value. In ambiguous scenarios, you must explicitly disambiguate by using the nested literal:

cfg := AppConfig{
    DatabaseConfig: DatabaseConfig{
        Host: "db.internal",
        Port: 5432,
    },
    CacheConfig: CacheConfig{
        Host: "cache.internal",
        TTL:  300,
    },
}

Strict Guardrail: No Split Initialization

Go enforces a strict compiler guardrail: you cannot mix promoted fields with their enclosing embedded struct literal in the same composite literal.

// ILLEGAL: mixing promoted field and enclosing embedded struct
cfg := AppConfig{
    DatabaseConfig: DatabaseConfig{Host: "localhost"},
    Port:           5432, // compile error!
}

The compiler halts compilation with an explicit error:

cannot specify promoted field Port and enclosing embedded field DatabaseConfig

Even though Host and Port do not collide, Go forbids splitting initialization across structural boundaries. You must choose one convention per embedded type: either initialize the embedded struct explicitly, or initialize its promoted fields from the outer literal.

Automated Migration with go fix -embedlit

You do not need to rewrite legacy nested composite literals by hand. Go 1.27 introduces the embedlit modernizer analyzer integrated into go fix:

# Preview proposed literal simplifications across your module
go fix -embedlit -diff ./...

# Apply automatic simplifications in place
go fix -embedlit ./...

The embedlit analyzer safely identifies composite literals where nested embedded type wrappers are redundant, modernizing the code while verifying that no field name collisions occur.

When to Keep Nesting

Flattened initialization is the sensible default for configs, DTOs, and test fixtures, but nested construction remains valuable in specific architectural scenarios:

  1. Resolving Ambiguities: When embedded types have colliding field names at the same depth.
  2. Explicit Domain Boundaries: In large structs with dozens of options (e.g., 30+ database parameters), grouping options inside DatabaseConfig: DatabaseConfig{...} signals to readers that those values belong to a distinct subsystem.
  3. Partial Initialization of Embedded Subsystems: When reusing or passing an existing pre-allocated instance of an embedded struct directly into the constructor.

Struct Tags

Metadata for serialization and other tools:

type User struct {
    ID        int    `json:"id" db:"user_id"`
    Name      string `json:"name,omitempty"`
    Password  string `json:"-"`  // Omit from JSON
    CreatedAt time.Time `json:"created_at"`
}

Reading Tags

import "reflect"

t := reflect.TypeOf(User{})
field, _ := t.FieldByName("Name")
tag := field.Tag.Get("json")  // "name,omitempty"

Comparison

Structs are comparable if all fields are comparable:

p1 := Person{Name: "Alice"}
p2 := Person{Name: "Alice"}
p1 == p2  // true

Constructors

Go uses factory functions:

func NewPerson(name string, age int) *Person {
    return &Person{
        Name:   name,
        Age:    age,
        Active: true,  // Default
    }
}

func NewPersonWithDefaults() *Person {
    return &Person{
        Active: true,
    }
}

Summary

Pattern Usage
Define type Name struct { fields }
Create Name{Field: value}
Embed Include type name without field name
Tags Field Type \key:“value”`| | Constructor |func NewType() *Type`

More examples

Example: literal and field access

Save as main.go and go run . (with go mod init example if needed).

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func main() {
    u := User{Name: "ada", Age: 36}
    fmt.Printf("%+v\n", u)
    u.Age++
    fmt.Println("age:", u.Age)
}

Expected:

{Name:ada Age:36}
age: 37

Example: embedding promotes fields

Save as main.go and go run . (with go mod init example if needed).

package main

import "fmt"

type Address struct {
    City string
}

type Person struct {
    Name string
    Address
}

func main() {
    p := Person{Name: "lin", Address: Address{City: "Berlin"}}
    fmt.Println(p.Name, "in", p.City) // promoted field
}

Expected:

lin in Berlin

Runnable example

Save as main.go. From an empty directory:

go mod init example
go run .
package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

type Address struct {
    Street string
    City   string
}

type Person struct {
    Name string
    Age  int
}

type Employee struct {
    Person  // embedded
    Address // embedded
    Title   string
}

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name,omitempty"`
    Password string `json:"-"`
}

func NewEmployee(name, title, city string) *Employee {
    return &Employee{
        Person:  Person{Name: name, Age: 0},
        Address: Address{City: city},
        Title:   title,
    }
}

func main() {
    var zero Person
    fmt.Printf("zero person: %+v\n", zero)

    e := NewEmployee("Ada", "Engineer", "London")
    e.Age = 36 // promoted field
    fmt.Printf("employee: name=%s title=%s city=%s age=%d\n", e.Name, e.Title, e.City, e.Age)

    // Go 1.27+: flattened struct literal initialization of promoted fields
    e2 := Employee{
        Name:  "Bob",
        Age:   28,
        City:  "Cambridge",
        Title: "Staff Engineer",
    }
    fmt.Printf("flattened employee: name=%s title=%s city=%s age=%d\n", e2.Name, e2.Title, e2.City, e2.Age)

    // Pointer field access auto-dereferences
    p := &Person{Name: "Grace", Age: 85}
    p.Age++
    fmt.Printf("pointer person: %+v\n", p)

    // Anonymous struct for one-off data
    point := struct{ X, Y int }{10, 20}
    fmt.Printf("point=(%d,%d)\n", point.X, point.Y)

    // Comparable when all fields are comparable
    a := Person{Name: "Ada", Age: 36}
    b := Person{Name: "Ada", Age: 36}
    fmt.Println("a==b?", a == b)

    // Tags for JSON + reflection
    u := User{ID: 7, Name: "Ada", Password: "secret"}
    raw, err := json.Marshal(u)
    if err != nil {
        fmt.Println("json error:", err)
        return
    }
    fmt.Println("json:", string(raw))

    t := reflect.TypeOf(User{})
    if f, ok := t.FieldByName("Name"); ok {
        fmt.Println("Name tag:", f.Tag.Get("json"))
    }
    if f, ok := t.FieldByName("Password"); ok {
        fmt.Println("Password tag:", f.Tag.Get("json"))
    }
}

Expected output (illustrative):

zero person: {Name: Age:0}
employee: name=Ada title=Engineer city=London age=36
flattened employee: name=Bob title=Staff Engineer city=Cambridge age=28
pointer person: &{Name:Grace Age:86}
point=(10,20)
a==b? true
json: {"id":7,"name":"Ada"}
Name tag: name,omitempty
Password tag: -

What to notice: - Embedding promotes fields (e.Name, e.City) without classical inheritance. - Go 1.27+ flattened struct literals allow initializing promoted fields (Name, Age, City) directly on Employee without nesting Person or Address. - Factory NewEmployee sets defaults; zero Person is still valid. - JSON tags control names, omitempty, and secrets (json:"-"). - Struct equality works when every field is comparable.

Try next: Set Name to "" and remarshal—observe omitempty dropping the field.