Day 3 — Maps, Structs & Methods

Updated

July 30, 2025

Day 3 — Maps, Structs & Methods

Day 6 — Maps

Stage I · ~3h (theory-heavy)
Goal: Use maps idiomatically for counts, indexes, and simple caches—mastering nil vs made maps, the comma-ok idiom, deletion, and the rule that iteration order is intentionally randomized.

Note

Maps are not safe for concurrent read/write without synchronization. Today you stay single-goroutine. Concurrent map use returns with the race detector in Stage III.

Why this day exists

Maps appear everywhere:

  • Frequency counts and histograms
  • Deduplication sets (map[T]struct{})
  • Indexes by ID
  • Memoization / caches
  • JSON objects decoding to map[string]any

Misunderstanding zero values, missing keys, and iteration order produces flaky tests and subtle production bugs. Day 6 locks the model.


Theory 1 — Type and creation

var m map[string]int          // nil map
m2 := map[string]int{}        // empty non-nil
m3 := make(map[string]int)    // empty non-nil
m4 := make(map[string]int, 100) // hint capacity
m5 := map[string]int{"a": 1, "b": 2}

Key and value types

  • Keys must be comparable (can use ==): numbers, strings, pointers, channels, interfaces, structs/arrays of comparable types
  • Slices, maps, and functions are not comparable → cannot be keys
  • Values may be any type, including slices and maps
type Coord struct{ X, Y int }
scores := map[Coord]int{{1, 2}: 10} // OK: struct of comparables

Theory 2 — Nil map vs empty map

Operation Nil map Empty made map
Read m[k] zero value zero value
len(m) 0 0
Write m[k]=v panic OK
delete(m, k) no-op OK
range no iterations no iterations
var m map[string]int
fmt.Println(m["x"]) // 0, no panic
// m["x"] = 1       // panic: assignment to entry in nil map
m = make(map[string]int)
m["x"] = 1          // OK

Law: always make (or literal) before first write.


Theory 3 — Lookup, comma-ok, and zero values

v := m["missing"] // 0 for int — cannot tell missing from stored zero

Comma-ok idiom

v, ok := m["missing"]
if !ok {
    // key absent
}
ok Meaning
true Key present (value may still be zero)
false Key absent; v is zero value

Presence-only sets

seen := make(map[string]struct{})
seen["a"] = struct{}{}
if _, ok := seen["a"]; ok {
    // present
}

struct{} uses no memory per value beyond the map’s own overhead of tracking the key.


Theory 4 — Mutation: assign, delete, update

m["a"] = 1
m["a"]++        // read-modify-write
delete(m, "a")  // safe if absent
delete(m, "z")  // no-op

Update with comma-ok

func increment(m map[string]int, k string) {
    m[k]++ // works even if absent: zero then +1
}

For non-increment cases:

if v, ok := m[k]; ok {
    m[k] = v + 10
} else {
    m[k] = 10
}

Maps hold values, not variables

type Counter struct{ N int }
m := map[string]Counter{"a": {N: 1}}
// m["a"].N++ // compile error: cannot assign to struct field in map
c := m["a"]
c.N++
m["a"] = c // write back

Or store pointers:

m := map[string]*Counter{"a": {N: 1}}
m["a"].N++ // OK

Theory 5 — Iteration order is random

m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
    fmt.Println(k, v)
}

Go randomizes map iteration order so programs do not accidentally depend on it. Tests that compare full map dump strings without sorting will flake.

Stable output pattern

keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
    fmt.Printf("%s=%d\n", k, m[k])
}

Theory 6 — Reference semantics

Map values are descriptors (like slices headers). Assignment and function parameters share the same underlying map:

a := map[string]int{"x": 1}
b := a
b["x"] = 2
fmt.Println(a["x"]) // 2
func clearAll(m map[string]int) {
    for k := range m {
        delete(m, k)
    }
}

To deep-copy:

func clone(m map[string]int) map[string]int {
    out := make(map[string]int, len(m))
    for k, v := range m {
        out[k] = v
    }
    return out
}

Theory 7 — Design patterns

Word count

func wordCount(text string) map[string]int {
    freq := make(map[string]int)
    for _, w := range strings.Fields(strings.ToLower(text)) {
        freq[w]++
    }
    return freq
}

Index by ID

func indexByID(users []User) map[string]User {
    m := make(map[string]User, len(users))
    for _, u := range users {
        m[u.ID] = u
    }
    return m
}

Simple TTL-less cache

type Cache struct {
    m map[string]string
}

func NewCache() *Cache {
    return &Cache{m: make(map[string]string)}
}

func (c *Cache) Get(k string) (string, bool) {
    v, ok := c.m[k]
    return v, ok
}

func (c *Cache) Set(k, v string) {
    c.m[k] = v
}

Not concurrent-safe. Good enough for single-threaded CLIs.

Grouping

func groupBy[T any, K comparable](items []T, key func(T) K) map[K][]T {
    out := make(map[K][]T)
    for _, item := range items {
        k := key(item)
        out[k] = append(out[k], item)
    }
    return out
}

Generics formal depth is Stage IV; the pattern works with concrete types today.


Worked examples bank

Example A — Nil write panic (observe once)

func main() {
    var m map[string]int
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    m["x"] = 1
}

Prefer fixing with make over recovering (Day 14).

Example B — Comma-ok vs zero value

m := map[string]int{"a": 0}
fmt.Println(m["a"])        // 0
fmt.Println(m["missing"])  // 0
_, ok1 := m["a"]           // true
_, ok2 := m["missing"]     // false

Example C — Frequency report sorted

func report(freq map[string]int) {
    type kv struct {
        k string
        v int
    }
    var list []kv
    for k, v := range freq {
        list = append(list, kv{k, v})
    }
    sort.Slice(list, func(i, j int) bool {
        if list[i].v != list[j].v {
            return list[i].v > list[j].v // count desc
        }
        return list[i].k < list[j].k
    })
    for _, e := range list {
        fmt.Printf("%s %d\n", e.k, e.v)
    }
}

Example D — Dedup preserving first-seen order

func unique(ss []string) []string {
    seen := make(map[string]struct{}, len(ss))
    out := make([]string, 0, len(ss))
    for _, s := range ss {
        if _, ok := seen[s]; ok {
            continue
        }
        seen[s] = struct{}{}
        out = append(out, s)
    }
    return out
}

Example E — Nested maps

// map[region]map[service]count
func inc(m map[string]map[string]int, region, service string) {
    inner, ok := m[region]
    if !ok {
        inner = make(map[string]int)
        m[region] = inner
    }
    inner[service]++
}

Always ensure the inner map is made before write.


Labs

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

Lab 1 — Word count CLI

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

Spec:

  1. Read stdin or a file path argument.
  2. Split on unicode/strings.Fields (whitespace).
  3. Normalize with strings.ToLower.
  4. Print word count lines sorted by count descending, then word ascending.
  5. Exit non-zero on I/O errors.
echo "Go go Gopher gopher GO" | go run .
# go 3
# gopher 2

Lab 2 — Set operations

Implement:

func union(a, b []string) []string
func intersect(a, b []string) []string
func difference(a, b []string) []string // in a not in b

Use maps. Document whether outputs are sorted (they should be for stable demos).

Lab 3 — Mini cache

In-memory string cache with Get/Set/Delete/Len. Drive it from a tiny REPL or flags:

set foo bar
get foo  → bar
get baz  → missing (exit or print status)

Common gotchas

Gotcha Fix
Write to nil map make first
Using value for presence when zero is valid Comma-ok
Flaky tests on map range order Sort keys
Concurrent map access Mutex / sync.Map later—not today
Cannot increment struct field in map Copy out, modify, write back—or store *T
Using slice as map key Use string key or comparable struct
Expecting maps to deep-copy on assign They share; clone explicitly

Checkpoint

  • Explain nil vs empty map
  • Comma-ok used correctly
  • delete and len used
  • Sorted output for deterministic CLI
  • Word count lab works
  • Know maps are reference-like and not concurrent-safe

Commit

git add .
git commit -m "day06: maps wordcount + set ops"

Tomorrow

Day 7 — Structs, embedding & methods: domain modeling with composite types, value vs pointer receivers, and promotion via embedding—without mistaking embedding for inheritance.


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.”