23. Structs: your own types
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.