23. Structs: your own types

📖 Reading · 11 min
💡 Every code box below is live — edit it and hit Run.

A struct groups related values into one thing with named fields. Go has no classes — structs plus methods are how you model everything, from a database row to an HTTP client.

Declaring and creating

package main

import "fmt"

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    p := Person{Name: "Ada", Age: 36, City: "London"}

    fmt.Println(p.Name, "is", p.Age)
    p.Age = 37
    fmt.Println(p)
}

type Person struct { ... } creates a brand new type. Fields are accessed with a dot, and assigned to like any variable.

Notice gofmt aligns the field types into a column — that's not you being tidy, that's the formatter, and every Go codebase looks like this.

Field names in literals (and why you should use them)

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {
    a := Point{X: 3, Y: 4}
    b := Point{1, 2}

    fmt.Println(a, b)
    fmt.Printf("%v | %+v\n", a, a)
}

Point{1, 2} — a positional literal — works but is fragile: add a field to the struct and every positional literal in your codebase either breaks or, worse, silently means something different. Use Field: value form except for tiny, stable types like Point.

%v prints the values, %+v adds the field names. %+v is the one you want when debugging.

The zero value is a usable value

Declare a struct without initialising it and every field takes its own zero value:

package main

import "fmt"

type Config struct {
    Host    string
    Port    int
    Debug   bool
    Retries int
}

func main() {
    var c Config
    fmt.Printf("%+v\n", c)

    partial := Config{Host: "localhost"}
    fmt.Printf("%+v\n", partial)

    if partial.Port == 0 {
        partial.Port = 8080
    }
    fmt.Printf("%+v\n", partial)
}

No constructor was needed and nothing is nil or undefined. Go leans on this hard: a well-designed struct should be useful at its zero value. sync.Mutex, bytes.Buffer and strings.Builder all work with zero setup for exactly this reason.

Omitted fields in a literal get zero values too — that's the partial case, and it's Go's substitute for optional arguments.

Structs are values

Like arrays, and unlike maps and slices, assigning a struct copies it:

package main

import "fmt"

type Counter struct {
    N int
}

func main() {
    a := Counter{N: 1}
    b := a
    b.N = 100

    fmt.Println("a:", a.N, "b:", b.N)

    bump := func(c Counter) {
        c.N += 50
    }
    bump(a)
    fmt.Println("after bump, a:", a.N)
}

b := a made an independent copy, and bump got its own copy too. If you want a function to modify a struct, you pass a pointer — the next lesson after this pair.

Comparing structs

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {
    fmt.Println(Point{1, 2} == Point{1, 2})
    fmt.Println(Point{1, 2} == Point{1, 3})

    m := map[Point]string{
        {0, 0}: "origin",
        {1, 1}: "diagonal",
    }
    fmt.Println(m[Point{0, 0}])
}

Structs support == if all their fields do, comparing field by field. That also makes them valid map keys — genuinely useful for grids, coordinate pairs and composite lookups.

A struct containing a slice or a map can't be compared; the compiler will tell you so.

Nesting

Struct fields can be structs:

package main

import "fmt"

type Address struct {
    Street string
    City   string
}

type Employee struct {
    Name    string
    Age     int
    Home    Address
    Skills  []string
}

func main() {
    e := Employee{
        Name: "Grace",
        Age:  45,
        Home: Address{Street: "12 Navy Rd", City: "Arlington"},
        Skills: []string{"Go", "COBOL"},
    }

    fmt.Println(e.Home.City)
    fmt.Println(e.Skills[0])

    e.Home.City = "Baltimore"
    fmt.Printf("%+v\n", e.Home)
}

e.Home.City chains as far as you need. There's a shorter way to compose types — embedding — which is the next lesson.

Slices of structs

The bread-and-butter shape of real Go programs:

package main

import (
    "fmt"
    "sort"
)

type Product struct {
    Name  string
    Price float64
    Stock int
}

func main() {
    inventory := []Product{
        {Name: "Keyboard", Price: 49.99, Stock: 12},
        {Name: "Monitor", Price: 199.50, Stock: 3},
        {Name: "Cable", Price: 9.99, Stock: 87},
    }

    sort.Slice(inventory, func(i, j int) bool {
        return inventory[i].Price < inventory[j].Price
    })

    total := 0.0
    for _, p := range inventory {
        fmt.Printf("%-10s $%7.2f x%d\n", p.Name, p.Price, p.Stock)
        total += p.Price * float64(p.Stock)
    }
    fmt.Printf("inventory value: $%.2f\n", total)
}

Inside a []Product literal you can drop the repeated type name — {Name: "Cable", ...} is enough, because Go already knows what the elements are.

One trap: for _, p := range inventory gives you a copy of each element. Assigning to p.Stock changes nothing. To modify in place, index: inventory[i].Stock = 0.

Your turn

Define a Book struct with Title (string), Author (string) and Pages (int). Create a slice of two books and print the total page count and the title of the longer one:

total pages: 650
longest: The Go Programming Language
package main

import "fmt"

// define Book here

func main() {
    books := []Book{
        {Title: "The Go Programming Language", Author: "Donovan & Kernighan", Pages: 380},
        {Title: "Learning Go", Author: "Bodner", Pages: 270},
    }
    // print the total pages and the title of the book with the most pages
}
package main

import "fmt"

type Book struct {
    Title  string
    Author string
    Pages  int
}

func main() {
    books := []Book{
        {Title: "The Go Programming Language", Author: "Donovan & Kernighan", Pages: 380},
        {Title: "Learning Go", Author: "Bodner", Pages: 270},
    }

    total := 0
    longest := books[0]
    for _, b := range books {
        total += b.Pages
        if b.Pages > longest.Pages {
            longest = b
        }
    }
    fmt.Println("total pages:", total)
    fmt.Println("longest:", longest.Title)
}

Next: how Go composes structs out of other structs — without inheritance.