Day 3 — Maps, Structs & Methods
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.
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 comparablesTheory 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 // OKLaw: 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 zeroComma-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-opUpdate 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 backOr store pointers:
m := map[string]*Counter{"a": {N: 1}}
m["a"].N++ // OKTheory 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"]) // 2func 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"] // falseExample 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/day06Spec:
- Read stdin or a file path argument.
- Split on
unicode/strings.Fields(whitespace).
- Normalize with
strings.ToLower.
- Print
word countlines sorted by count descending, then word ascending.
- Exit non-zero on I/O errors.
echo "Go go Gopher gopher GO" | go run .
# go 3
# gopher 2Lab 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 bUse 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
deleteandlenused
- 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.
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.”