Go / Basic: Values and Program Contracts / Lesson 5
Maps, Structs, and Data Invariants
Choose keyed lookup and records deliberately, and preserve invariants without inventing map ordering or deep-copy guarantees.
On this page
Lesson goals
- Use map lookup with presence information.
- Produce deterministic output without relying on map iteration order.
- Explain structure copies containing reference-bearing fields.
A map associates comparable keys with values
A map provides keyed access rather than positional order. Keys must have a comparable type; slices, maps, and functions are not ordinary comparable keys. A missing lookup returns the element type’s zero value. Use value, ok := m[key] when a stored zero must be distinguished from absence. len reports current entries, not allocated capacity.
A nil map supports lookup, length, range, and deletion, but assigning an entry panics. Initialize writable maps with make or a literal. Copying a map value shares the underlying map; it is not an independent duplicate. Concurrent access needs a policy whenever a writer can overlap another access.
Sort keys when order is part of the output
Map iteration order is unspecified and is not guaranteed to remain the same from one iteration to the next. It is not a random-number API and should not be called a guaranteed shuffle. The example gathers keys, sorts them, and then performs lookups in that explicit order.
For a map of names to counts, a missing name and a stored zero count have different meanings. The comma-ok lookup exposes that difference before output. If keys or values can change concurrently, merely sorting a key snapshot does not make subsequent map reads safe; the synchronization or snapshot ownership must cover the whole operation.
package main
import (
"fmt"
"sort"
)
func main() {
counts := map[string]int{"pear": 2, "apple": 0}
_, present := counts["missing"]
keys := make([]string, 0, len(counts))
for key := range counts {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("%s=%d\n", key, counts[key])
}
fmt.Println("missing present", present)
}
apple=0
pear=2
missing present false
The sort establishes apple before pear; the missing lookup reports false.
A record connects fields logically
A struct groups named fields with potentially different types. Composite literals with field names make intent clear and leave omitted fields at their zero values. Exported identifiers begin with an uppercase letter, but exported storage also lets callers bypass validation. Keep invariant-sensitive fields private when all changes must pass through an operation.
For a booking, start must not follow end. For a buffer, logical length must not exceed capacity. These relationships define valid states beyond each field’s individual type. Constructors and mutating methods must preserve them, and failure should leave a documented state rather than partially updating a record.
A struct assignment copies its field values
The next example copies a record containing a string and a slice. Replacing copy.Name changes only the copied string field, while changing copy.Scores[0] reaches the shared backing array. The assignment is a complete value copy of the struct; sharing remains because the copied slice field itself describes existing storage.
This rule also applies to maps and pointers stored inside records. A constructor does not automatically confer exclusive ownership over supplied collections. Choose between borrowing input, taking an ownership-like responsibility by convention, or making a copy. Document the choice and ensure tests exercise mutations from both sides.
package main
import "fmt"
type Record struct {
Name string
Scores []int
}
func main() {
original := Record{Name: "Ada", Scores: []int{1, 2}}
copied := original
copied.Name = "Bo"
copied.Scores[0] = 9
fmt.Println(original.Name, original.Scores, copied.Name, copied.Scores)
}
Ada [9 2] Bo [9 2]
The names are independent field values, while the scores remain aliased.
Keep mutation policies simple
During map range, an entry removed before it is reached will not be produced; an entry created during iteration may or may not be produced. Do not build essential processing around those alternatives. Gather a work list or perform a separate update phase when a clear snapshot-like result is required.
A map entry is not an addressable struct variable: updating one of its struct fields directly is not generally permitted. Retrieve the value, modify it, and assign it back, or deliberately store pointers with an explicit sharing policy. Pointer storage changes lifetime and concurrency concerns; it is not merely a syntax workaround.
Common errors and fixes
A stored zero is treated as a missing key.
- Why it happens
- The lookup ignored its presence result.
- How to fix it
- Use the comma-ok form.
- Prevent it next time
- Test absent and explicitly zero-valued entries.
Output order changes between runs.
- Why it happens
- Map range was used as an ordering contract.
- How to fix it
- Sort keys or use an ordered representation.
- Prevent it next time
- Make ordering explicit in tests and serialization.
A copied record unexpectedly changes another record’s collection.
- Why it happens
- A slice or map field still shares storage.
- How to fix it
- Copy the referenced data when isolation is required.
- Prevent it next time
- Document constructor and copy aliasing.
Check your understanding
1. A map stores account balances and zero is valid. Design a lookup contract that distinguishes an unknown account.
Show worked solution
Return balance and a presence flag, or translate absence into a documented error. Use the comma-ok result instead of balance == 0. Tests should cover an absent key, a stored zero, and a positive balance. If the map is shared with writers, protect the lookup under the same synchronization policy as updates.
2. How would you make the copied Record’s Scores independent while keeping its Name field? Explain the nested-data limit.
Show worked solution
First copy the record, then assign copied.Scores = append([]int(nil), original.Scores...). Its int elements now live in separate storage, so modifying either slice’s elements does not affect the other. The string field is already immutable as a value. If Scores instead contained slices or pointers, this outer element copy would not recursively duplicate their targets.
Key takeaways
- Presence, value, and output order are separate map concerns.
- Struct invariants connect fields beyond their individual types.
- A value copy can include descriptors of shared storage.