Day 7 — Structs, embedding & methods

Updated

July 30, 2026

Day 7 — Structs, embedding & methods

Stage I · ~3h (theory-heavy)
Goal: Model a small domain with structs, choose value vs pointer receivers deliberately, and use embedding for composition—without treating Go as class-based OOP.

Note

Embedding promotes methods and fields; it is not subclassing. There is no virtual inheritance tree. Interfaces (Day 11) supply polymorphism.

Why this day exists

Real Go programs are not bags of primitives. They are:

  • Structs for entities and DTOs
  • Methods for behavior bound to types
  • Embedding for “has-a / is-composed-of” reuse
  • Clear rules about mutability (pointer receivers)

If you invent a Java-style class hierarchy in Go, packages become painful. Day 7 teaches the idiomatic shapes used across the stdlib (time.Time, net/http.Request, bytes.Buffer, …).


Theory 1 — Struct definition and literals

type Item struct {
    SKU   string
    Name  string
    Qty   int
    PriceCents int
}

var zero Item              // all fields zero values
a := Item{"A1", "Widget", 2, 500} // positional — fragile
b := Item{SKU: "A1", Name: "Widget", Qty: 2, PriceCents: 500} // keyed — preferred
c := Item{SKU: "B2"}       // other fields zero

Exported fields

Name Visible outside package?
Name Yes
name No (same package only)

Field names are part of your package API when exported.

Anonymous / nested structs

var cfg struct {
    Host string
    Port int
}
cfg.Host = "localhost"
cfg.Port = 8080

Useful for one-off JSON or tests; prefer named types for domain models.

Comparison

Structs are comparable if all fields are comparable:

type Pair struct{ A, B int }
fmt.Println(Pair{1, 2} == Pair{1, 2}) // true

Structs containing slices/maps/functions are not comparable with ==.


Theory 2 — Methods and receivers

func (i Item) TotalCents() int {
    return i.Qty * i.PriceCents
}

func (i *Item) Add(n int) {
    i.Qty += n
}

Value receiver

  • Method sees a copy of the struct
  • Mutations to fields do not affect the caller
  • Fine for small immutable-ish views and pure calculations

Pointer receiver

  • Method sees the same struct
  • Mutations persist
  • Avoids copying large structs
  • Required if any method mutates—or keep the whole method set consistent

Consistency rule (stdlib style)

If any method on T needs a pointer receiver, prefer pointer receivers for the whole set so the method set is predictable when assigning to interfaces (Day 11).

func (i *Item) Add(n int)     { i.Qty += n }
func (i *Item) TotalCents() int { return i.Qty * i.PriceCents }

Calling sugar

i := Item{Qty: 1, PriceCents: 100}
i.Add(2)       // Go may take address automatically
p := &i
p.TotalCents() // Go may dereference automatically

You still choose receiver type by semantics, not by call syntax.

Nil receivers

func (i *Item) Label() string {
    if i == nil {
        return "<nil item>"
    }
    return i.Name
}

var p *Item
fmt.Println(p.Label()) // OK if method handles nil

Useful for optional values; document the contract.


Theory 3 — Embedding and promotion

type Named struct {
    Name string
}

func (n Named) Label() string { return n.Name }

type Product struct {
    Named // embedded
    SKU   string
    PriceCents int
}

p := Product{Named: Named{Name: "Gadget"}, SKU: "G1", PriceCents: 999}
fmt.Println(p.Name)    // promoted field
fmt.Println(p.Label()) // promoted method

What embedding is

  • Composition: Product has a Named inside
  • Promoted selectors: outer type gains inner fields/methods at the top level
  • The inner type is still addressable as p.Named

What embedding is not

  • Not inheritance: no “subclass” relationship
  • Not dynamic dispatch by itself: method expressions still resolve statically unless interfaces are used
  • Overriding: defining Product.Label shadows promoted Named.Label for Product values
func (p Product) Label() string {
    return p.SKU + ":" + p.Named.Label()
}

Multiple embedding and conflicts

If two embedded types promote the same name, the outer type must qualify the selector (or define its own method). Ambiguous selectors are compile errors at use sites.

Embedding pointers

type Bag struct {
    *Named
}

Useful but nil-careful: promoted methods may panic if the pointer is nil and the method does not guard.


Theory 4 — Constructor functions and zero values

Go has no constructors. Idioms:

func NewItem(sku, name string, priceCents int) Item {
    return Item{SKU: sku, Name: name, PriceCents: priceCents, Qty: 0}
}

func NewItemPtr(sku, name string, priceCents int) *Item {
    return &Item{SKU: sku, Name: name, PriceCents: priceCents}
}

When zero value is ready to use

bytes.Buffer, sync.Mutex (must not be copied after use), and many stdlib types are designed so var x T works.

Design your types similarly when possible:

type Inventory struct {
    items map[string]*Item
}

func (inv *Inventory) ensure() {
    if inv.items == nil {
        inv.items = make(map[string]*Item)
    }
}

func (inv *Inventory) Put(it *Item) {
    inv.ensure()
    inv.items[it.SKU] = it
}

Or require NewInventory() and document that zero value is invalid—be explicit.


Theory 5 — Struct tags (awareness)

type UserDTO struct {
    ID    string `json:"id"`
    Email string `json:"email,omitempty"`
}

Tags are strings consumed via reflection (encoding/json, etc.). You will use them deeply in Stage V; today: know they annotate fields without changing Go type rules.


Theory 6 — Memory layout intuition (light)

Fields are laid out in order with padding for alignment. Reordering fields can change size:

type Bad struct {
    A bool
    B int64
    C bool
} // more padding

type Better struct {
    B int64
    A bool
    C bool
} // typically smaller

Do not micro-optimize early; know that field order can matter for large high-volume structs later (pprof stage).


Worked examples bank

Example A — Inventory domain

package inventory

import "fmt"

type Item struct {
    SKU        string
    Name       string
    Qty        int
    PriceCents int
}

func (i Item) TotalCents() int {
    return i.Qty * i.PriceCents
}

func (i *Item) Restock(n int) error {
    if n < 0 {
        return fmt.Errorf("restock negative: %d", n)
    }
    i.Qty += n
    return nil
}

func (i *Item) Take(n int) error {
    if n < 0 {
        return fmt.Errorf("take negative: %d", n)
    }
    if i.Qty < n {
        return fmt.Errorf("sku %s: have %d want %d", i.SKU, i.Qty, n)
    }
    i.Qty -= n
    return nil
}

type Store struct {
    bySKU map[string]*Item
}

func NewStore() *Store {
    return &Store{bySKU: make(map[string]*Item)}
}

func (s *Store) Add(it Item) error {
    if it.SKU == "" {
        return fmt.Errorf("empty sku")
    }
    if _, exists := s.bySKU[it.SKU]; exists {
        return fmt.Errorf("duplicate sku %s", it.SKU)
    }
    cp := it
    s.bySKU[it.SKU] = &cp
    return nil
}

func (s *Store) Get(sku string) (*Item, bool) {
    it, ok := s.bySKU[sku]
    return it, ok
}

Example B — Embedding for composition

type Timestamped struct {
    CreatedUnix int64
    UpdatedUnix int64
}

func (t *Timestamped) Touch(now int64) {
    if t.CreatedUnix == 0 {
        t.CreatedUnix = now
    }
    t.UpdatedUnix = now
}

type Record struct {
    Timestamped
    ID   string
    Body string
}

Example C — Value receiver purity

func (i Item) WithQty(n int) Item {
    i.Qty = n
    return i // returns modified copy
}

Functional update style without pointers.

Example D — Method sets preview

type Counter struct{ N int }

func (c *Counter) Inc() { c.N++ }

// var _ interface{ Inc() } = Counter{}  // fails: Inc has pointer receiver
var _ interface{ Inc() } = &Counter{}    // OK

Day 11 expands this into full interface theory.

Example E — Unexported fields + accessors

type Account struct {
    id      string
    balance int
}

func OpenAccount(id string) *Account {
    return &Account{id: id}
}

func (a *Account) ID() string { return a.id }

func (a *Account) Balance() int { return a.balance }

func (a *Account) Deposit(cents int) error {
    if cents <= 0 {
        return fmt.Errorf("deposit must be positive")
    }
    a.balance += cents
    return nil
}

Encapsulation via unexported fields is package-level, not type-private like some languages.


Labs

Suggested workspace: ~/lab/90daysofx/go/day07

Lab 1 — Inventory CLI

mkdir -p ~/lab/90daysofx/go/day07
cd ~/lab/90daysofx/go/day07
go mod init example.com/day07

Commands (flags or subcommands—keep parsing simple):

add <sku> <name> <price_cents>
restock <sku> <n>
take <sku> <n>
get <sku>
list
total   # sum of Qty*PriceCents for all items

Requirements:

  1. Item + Store (or Inventory) types with methods.
  2. At least one pointer receiver mutation and one value (or pure) calculation method.
  3. Errors for missing SKU, duplicates, insufficient qty.
  4. main / run split from Day 4.

In-memory only is fine; optional file load/save is stretch.

Lab 2 — Embedding exercise

Add a Meta embedded struct (Created, Note string) to Item or a wrapper type. Promote a String() or Label() method. Show both item.Note and item.Meta.Note access.

Lab 3 — Receiver consistency journal

Write a short NOTES.md:

  • Which methods needed pointers and why
  • What would break if Take used a value receiver
  • Whether zero Store is valid

Common gotchas

Gotcha Fix
Value receiver mutation “not sticking” Use pointer receiver
Treating embedding as inheritance Compose + interfaces
Positional struct literals across packages Use keyed fields
Copying struct with internal map/slice shallowly Document ownership; clone if needed
Exporting fields you meant to encapsulate Unexport + methods
Mixing value and pointer receivers randomly Keep method set consistent
Map of structs mutating fields Store *Item or write back full struct

Checkpoint

  • Keyed struct literals used
  • Value vs pointer receivers explained with an example
  • Embedding promotes a field and a method
  • Inventory-style domain CLI works
  • Errors returned for domain failures
  • Notes on zero-value readiness

Commit

git add .
git commit -m "day07: structs methods embedding inventory"

Tomorrow

Day 8 — Pointers & escape analysis: when pointers are necessary, what *T and &x mean, and how to read go build -gcflags=-m on your Day 7 code without cargo-culting “everything on the heap.”