Skip to content

06 · Structs

Defining and creating a struct

A struct groups related fields into a single named type — Go's equivalent of a lightweight class body without inheritance.

package main

import "fmt"

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    // Field names -- order doesn't matter, all fields required unless omitted
    p1 := Person{Name: "Alice", Age: 30, City: "Boston"}

    // Positional -- order MUST match the struct definition exactly
    p2 := Person{"Bob", 25, "Chicago"}

    // Zero-valued struct, filled in afterward
    var p3 Person
    p3.Name = "Carol"
    p3.Age = 28

    fmt.Println(p1, p2, p3)
    fmt.Println(p1.Name, p1.Age) // Alice 30
}

Structs are value types

Assigning or passing a struct copies it — unlike in Java or Python where objects are references. This matters constantly:

package main

import "fmt"

type Point struct {
    X, Y int
}

func tryToModify(p Point) {
    p.X = 999 // modifies the COPY, not the caller's original
}

func main() {
    original := Point{X: 1, Y: 2}
    tryToModify(original)
    fmt.Println(original) // {1 2} -- unchanged

    copy := original
    copy.X = 100
    fmt.Println(original, copy) // {1 2} {100 2} -- independent
}

To actually mutate the caller's struct, pass a pointer (see Module 7).

Nested structs

package main

import "fmt"

type Address struct {
    Street string
    City   string
}

type Employee struct {
    Name    string
    Address Address // struct field inside a struct
}

func main() {
    e := Employee{
        Name: "Dana",
        Address: Address{
            Street: "1 Main St",
            City:   "Denver",
        },
    }

    fmt.Println(e.Address.City) // Denver
    e.Address.City = "Boulder"  // dotted access reaches into nested fields
    fmt.Println(e)
}

Anonymous fields (embedding) and struct methods

Embedding a type gives the outer struct direct access to the embedded type's fields and methods -- Go's alternative to classical inheritance, covered in depth in Level 2.

package main

import "fmt"

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

type Dog struct {
    Animal // embedded -- Dog "has" all of Animal's fields/methods promoted
    Breed  string
}

func main() {
    d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
    fmt.Println(d.Name)    // Rex -- promoted field, no need for d.Animal.Name
    fmt.Println(d.Speak()) // Rex makes a sound -- promoted method
}

Struct tags and comparing structs

package main

import "fmt"

type Config struct {
    Host string `json:"host"` // tags are metadata, read by reflection-based
    Port int    `json:"port"` // packages like encoding/json (Level 2)
}

func main() {
    a := Config{Host: "localhost", Port: 8080}
    b := Config{Host: "localhost", Port: 8080}

    // Structs with only comparable fields support == directly
    fmt.Println(a == b) // true -- compares field by field
}

How It Actually Works

A struct is laid out in memory as its fields, in declaration order, each padded so it starts at an address that's a multiple of its own alignment requirement — that's why reordering fields from bool, int64, bool to int64, bool, bool shrinks a struct from 24 bytes to 16: the compiler has to pad each bool up to 8 bytes in the first layout to keep int64 aligned, but can pack both bools into one 8-byte slot in the second. There's no struct "boxing" — a struct value embedded in another struct is inlined byte-for-byte into the parent's memory, not stored as a pointer, which is why unsafe.Sizeof on a struct is roughly the sum (with padding) of its fields' sizes, not one pointer-width. Comparing structs with == compiles to a field-by-field (or block memcmp when possible) comparison done entirely at compile time by generating the comparison code — there's no runtime reflection involved unless you explicitly use reflect.DeepEqual, which is far slower because it walks types dynamically.

Cheat sheet

Feature Syntax
Define type Person struct { Name string; Age int }
Create (named fields) Person{Name: "A", Age: 1}
Create (positional) Person{"A", 1}
Zero value var p Person
Access/set field p.Name, p.Name = "X"
Nested access e.Address.City
Embedding type Dog struct { Animal; Breed string }
Compare (comparable fields) a == b
Struct tag `json:"host"`

🔀 See this in another language

Exercise

Define a Book struct with Title, Author, and Pages fields. Write a function isLong(b Book) bool that returns whether Pages > 300. In main, create a slice of three Book values and loop over them, printing each title along with whether it's "long."