CHAPTER 15 · COMPOSITE TYPES

Structs & Embedding

A struct is a named group of fields. It's how Go builds domain types, User, Order, Request, anything with multiple attributes. Structs replace "classes" from object-oriented languages, but with a simpler, composition-first mindset.

Learning objectives

  • Define named and anonymous structs.
  • Initialize structs with positional or named fields (and prefer named).
  • Use embedded fields to compose types, not inherit.
  • Attach metadata via struct tags (JSON, validate, etc).
  • Compare structs with == and know when you can't.

Defining a struct

type Person struct {
    Name string
    Age  int
}

Conventions: capitalize field names to export them (visible outside the package, Chapter 18). Keep related fields together.

Initialization

// Zero value
var p Person              // {Name:"" Age:0}

// Named fields (PREFERRED, survives refactors)
p := Person{Name: "Alice", Age: 30}

// Positional (fragile, avoid in production code)
p := Person{"Alice", 30}

// Partial, other fields get zero values
p := Person{Name: "Alice"}

Field access

p := Person{Name: "Alice", Age: 30}
fmt.Println(p.Name)       // Alice
p.Age++                    // 31

Access works through both values and pointers, Go auto-dereferences:

pp := &p
fmt.Println(pp.Name)       // Alice, no explicit (*pp).Name needed

Nested and anonymous structs

type Address struct { City, Country string }
type User struct {
    Name    string
    Address Address        // nested
}

// Anonymous struct, inline, one-off
config := struct {
    Host string
    Port int
}{
    Host: "localhost",
    Port: 8080,
}

Anonymous structs are handy in tests and one-shot data carriers. Don't use them for anything reused, give it a name.

Embedding: composition, not inheritance

Instead of class inheritance, Go has embedded fields , declare a field with only a type name, and that type's fields and methods are promoted:

type Entity struct {
    ID        int
    CreatedAt time.Time
}

type User struct {
    Entity                  // embedded, no field name!
    Name string
    Email string
}

u := User{
    Entity: Entity{ID: 1, CreatedAt: time.Now()},
    Name:   "Alice",
}

fmt.Println(u.ID)           // 1, promoted from Entity
fmt.Println(u.Entity.ID)    // 1: also accessible explicitly

This is Go's idiom for sharing common fields and behavior. User is not an Entity (no inheritance), but it has one and its fields look like they belong to User. Methods work the same way: if Entity has a Save() method, u.Save() works too (Chapter 20).

Struct tags (metadata)

A tag is a backtick-quoted string after a field. It's metadata that other packages (like encoding/json) read via reflection:

type User struct {
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`
    pwd   string `json:"-"`          // unexported, also excluded
}

Tags look like magic strings but follow a convention: key:"value,option1,option2". Common tag keys: json, yaml, xml, validate, db. We'll see them again in Chapter 29 (JSON).

Comparing structs

Structs are ==-comparable if all their fields are:

p1 := Person{"Alice", 30}
p2 := Person{"Alice", 30}
fmt.Println(p1 == p2)         // true

Struct containing a slice, map, or function? Not comparable. Use reflect.DeepEqual for those, or write a method:

type Order struct {
    Items []string   // slice makes this non-comparable with ==
}

The empty struct, struct{}

A struct with no fields takes zero bytes. Useful as a "marker" value:

// A "set" in Go
set := map[string]struct{}{
    "apple":  {},
    "banana": {},
}
if _, ok := set["apple"]; ok { /* … */ }

// Signal-only channel, Chapter 25
done := make(chan struct{})
close(done)

Check your understanding

Practice exercises

EXERCISE 1

Model a Book

Define a Book struct with Title, Author, Year (int), and Tags ([]string). Write a function summarize(b Book) string that returns e.g. "The Go Programming Language by Donovan & Kernighan (2015)".

Show solution
type Book struct {
    Title  string
    Author string
    Year   int
    Tags   []string
}

func summarize(b Book) string {
    return fmt.Sprintf("%s by %s (%d)", b.Title, b.Author, b.Year)
}
EXERCISE 2

Embedding in practice

Define a Timestamped struct with CreatedAt and UpdatedAt time fields. Then define Order and User structs that both embed Timestamped. Confirm that u.CreatedAt works.

Show solution
import "time"

type Timestamped struct {
    CreatedAt time.Time
    UpdatedAt time.Time
}

type User struct {
    Timestamped
    Name  string
    Email string
}

type Order struct {
    Timestamped
    ID    int
    Total float64
}

func main() {
    u := User{
        Timestamped: Timestamped{CreatedAt: time.Now()},
        Name:        "Alice",
    }
    fmt.Println(u.CreatedAt)   // promoted field
}

Further reading

Structs, the shape of your data.