Day 7 — Structs, embedding & methods
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.
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 zeroExported 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 = 8080Useful 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}) // trueStructs 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 automaticallyYou 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 nilUseful 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 methodWhat embedding is
- Composition:
Producthas aNamedinside
- 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.Labelshadows promotedNamed.LabelforProductvalues
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 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 smallerDo 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{} // OKDay 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/day07Commands (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:
Item+Store(orInventory) types with methods.
- At least one pointer receiver mutation and one value (or pure) calculation method.
- Errors for missing SKU, duplicates, insufficient qty.
main/runsplit 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
Takeused a value receiver
- Whether zero
Storeis 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.”