Day 06: Maps and Method Sets
Maps
A map[K]V is Go’s built-in hash table. The zero value is nil — always initialise before writing.
m := make(map[string]int)
m["apple"]++
m["banana"] = 5
// Comma-ok idiom — distinguish missing from zero
count, ok := m["apple"]
if !ok {
fmt.Println("key not found")
}
delete(m, "apple")
// Iteration — order is randomised
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
}
Pre-size hint when the approximate number of entries is known:
freq := make(map[string]int, len(words))
Embedded Structs (Composition)
Go favours composition over inheritance. Embed a type to promote its fields and methods:
type Animal struct {
Name string
}
func (a Animal) Speak() string { return a.Name + " makes a sound" }
type Dog struct {
Animal // embedded — promotes Name and Speak
Breed string
}
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(d.Name) // promoted from Animal
fmt.Println(d.Speak()) // promoted method
The embedded type’s methods are promoted — Dog satisfies any interface Animal satisfies, unless Dog overrides the method.
Method Sets and Interfaces
A type’s method set determines which interfaces it satisfies:
- A value
Thas all methods with value receivers - A pointer
*Thas all methods with value and pointer receivers
This matters when storing values in interfaces:
type Stringer interface { String() string }
type Foo struct{}
func (f *Foo) String() string { return "foo" }
var s Stringer = &Foo{} // ok — *Foo has the method
var t Stringer = Foo{} // compile error — Foo does not have String
Labs
Lab 1: Map Basics — CRUD and the Comma-Ok Idiom
What you’ll practise: Creating a map, reading with the two-value form, updating, deleting, and iterating.
Task: Build a map of country capitals, demonstrate every map operation, and show what happens when you read a missing key.
Steps:
- Create
capitals := map[string]string{"France": "Paris", "Japan": "Tokyo"} - Add a new entry, update an existing one, delete one
- Read a present key (comma-ok:
v, ok := ...) - Read a missing key — show it returns the zero value, not a panic
- Iterate with
range— note that order is random
package main
import "fmt"
func main() {
capitals := map[string]string{
"France": "Paris",
"Japan": "Tokyo",
"Brazil": "Brasília",
}
// Add and update
capitals["Germany"] = "Berlin"
capitals["France"] = "Paris (updated)"
// Delete
delete(capitals, "Brazil")
// Comma-ok idiom
if v, ok := capitals["Japan"]; ok {
fmt.Println("Japan's capital:", v)
}
// Missing key — returns zero value, not a panic
v := capitals["Australia"]
fmt.Printf("Australia (missing): %q (empty string)\n", v)
_, ok := capitals["Australia"]
fmt.Printf("Australia exists: %v\n", ok)
// Iteration — order is non-deterministic
fmt.Println("\nAll capitals:")
for country, city := range capitals {
fmt.Printf(" %s: %s\n", country, city)
}
}
Expected output:
Japan's capital: Tokyo
Australia (missing): "" (empty string)
Australia exists: false
All capitals:
France: Paris (updated)
Japan: Tokyo
Germany: Berlin
(order may vary)
Checkpoint: Reading a missing key never panics — it returns the zero value. Run the program multiple times and observe that the iteration order changes each time.
Lab 2: Map Patterns — Frequency, Grouping, and Sets
What you’ll practise: Three essential map patterns: word frequency counter, grouping items by a key, and using map[K]struct{} as a set.
Task: Implement all three patterns against a sample word list.
Steps:
- Frequency: count occurrences of each word in a sentence
- Grouping: group words by their first letter (
map[string][]string) - Set: deduplicate a list using
map[string]struct{}
package main
import (
"fmt"
"strings"
)
func main() {
sentence := "the cat sat on the mat the cat wore a hat"
words := strings.Fields(sentence)
// 1. Frequency counter
freq := make(map[string]int)
for _, w := range words {
freq[w]++
}
fmt.Println("Frequency:")
for w, n := range freq {
fmt.Printf(" %-6s %d\n", w, n)
}
// 2. Group words by first letter
groups := make(map[string][]string)
for _, w := range words {
key := string(w[0])
groups[key] = append(groups[key], w)
}
fmt.Println("\nGroups by first letter:")
for k, ws := range groups {
fmt.Printf(" %s: %v\n", k, ws)
}
// 3. Set — deduplicate
seen := make(map[string]struct{})
var unique []string
for _, w := range words {
if _, ok := seen[w]; !ok {
seen[w] = struct{}{}
unique = append(unique, w)
}
}
fmt.Printf("\nUnique words (%d): %v\n", len(unique), unique)
}
Expected output:
Frequency:
the 3
cat 2
sat 1
on 1
mat 1
wore 1
a 1
hat 1
(order varies)
Groups by first letter:
t: [the cat sat... ]
...
Unique words (8): [the cat sat on mat wore a hat]
Checkpoint: The frequency map correctly counts “the” as 3 and “cat” as 2. The set produces exactly 8 unique words. struct{} uses zero bytes — it is the idiomatic Go set value.
Lab 3: Methods — Receivers, Expressions, and Values
What you’ll practise: Defining value and pointer receiver methods on a struct, and understanding the difference between a method expression and a method value.
Task:
Build a Rectangle struct with Area() and Scale() methods, then demonstrate method expressions and method values.
Steps:
- Define
type Rectangle struct { Width, Height float64 } - Write
(r Rectangle) Area() float64— value receiver, read-only - Write
(r Rectangle) Perimeter() float64— value receiver - Write
(r *Rectangle) Scale(factor float64)— pointer receiver, mutates - Demonstrate a method value:
areaFn := r.Area(bound tor) - Demonstrate a method expression:
expr := Rectangle.Area(unbound, takes receiver as first arg)
package main
import "fmt"
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
r := Rectangle{Width: 4, Height: 3}
fmt.Printf("Area: %.1f\n", r.Area())
fmt.Printf("Perimeter: %.1f\n", r.Perimeter())
r.Scale(2)
fmt.Printf("After Scale(2): %+v\n", r)
// Method value — bound to r; calling areaFn() is like calling r.Area()
areaFn := r.Area
fmt.Printf("Method value Area(): %.1f\n", areaFn())
// Method expression — unbound; first arg is the receiver
expr := Rectangle.Area
r2 := Rectangle{Width: 5, Height: 5}
fmt.Printf("Method expression Area(r2): %.1f\n", expr(r2))
}
Expected output:
Area: 12.0
Perimeter: 14.0
After Scale(2): {Width:8 Height:6}
Method value Area(): 48.0
Method expression Area(r2): 25.0
Checkpoint: r.Scale(2) changes r because Scale has a pointer receiver. areaFn() captures the current state of r at the time the method value was created.
Lab 4: fmt.Stringer — Make Your Types Print Themselves
What you’ll practise: Implementing the fmt.Stringer interface so fmt.Println automatically calls your String() method.
Task:
Implement String() string on Rectangle and a Color type, then show how fmt automatically uses it in Println, Printf %v, and Sprintf.
Steps:
- Add
(r Rectangle) String() stringto the Rectangle from Lab 3 - Define
type Color struct { R, G, B uint8 }with a hexString() string - Call
fmt.Println(rect)andfmt.Println(color)— no extra args needed - Use
fmt.Sprintf("%v", rect)and confirm it also callsString()
package main
import "fmt"
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 { return r.Width * r.Height }
// String implements fmt.Stringer — called by fmt.Println, %v, %s
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle(%.1f x %.1f, area=%.1f)", r.Width, r.Height, r.Area())
}
type Color struct{ R, G, B uint8 }
func (c Color) String() string {
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
}
func main() {
r := Rectangle{Width: 10, Height: 5}
c := Color{R: 255, G: 128, B: 0}
// fmt.Println calls String() automatically
fmt.Println(r)
fmt.Println(c)
// Also works with %v (but not %d — that would use the struct format)
fmt.Printf("rect: %v\n", r)
fmt.Printf("color: %v\n", c)
// Sprintf works too
s := fmt.Sprintf("my rect is %v", r)
fmt.Println(s)
}
Expected output:
Rectangle(10.0 x 5.0, area=50.0)
#FF8000
rect: Rectangle(10.0 x 5.0, area=50.0)
color: #FF8000
my rect is Rectangle(10.0 x 5.0, area=50.0)
Checkpoint: Remove String() from Rectangle — fmt.Println(r) will then print {10 5}. Re-add it to see the difference. Try fmt.Printf("%#v", r) with and without String().
Lab 5: Map of Functions — Command Dispatcher
What you’ll practise: Using map[string]func([]string) to build a simple command dispatcher that routes input to registered handlers.
Task:
Build a dispatcher that registers “add”, “mul”, “upper”, and “help” commands, then dispatches to the correct handler based on a command name string.
Steps:
- Define
type Command func(args []string) - Create
dispatch := map[string]Command{...}and register four commands - Write a
run(dispatch map[string]Command, name string, args []string)function - Call
runwith each command name and confirm dispatch works - Call
runwith an unknown command — print a helpful error
package main
import (
"fmt"
"strconv"
"strings"
)
type Command func(args []string)
func run(dispatch map[string]Command, name string, args []string) {
cmd, ok := dispatch[name]
if !ok {
fmt.Printf("unknown command %q. Try 'help'\n", name)
return
}
cmd(args)
}
func main() {
dispatch := map[string]Command{
"add": func(args []string) {
sum := 0
for _, a := range args {
n, _ := strconv.Atoi(a)
sum += n
}
fmt.Printf("add(%v) = %d\n", args, sum)
},
"mul": func(args []string) {
product := 1
for _, a := range args {
n, _ := strconv.Atoi(a)
product *= n
}
fmt.Printf("mul(%v) = %d\n", args, product)
},
"upper": func(args []string) {
fmt.Println(strings.ToUpper(strings.Join(args, " ")))
},
"help": func(args []string) {
fmt.Println("commands: add, mul, upper, help")
},
}
run(dispatch, "add", []string{"1", "2", "3", "4"})
run(dispatch, "mul", []string{"2", "3", "5"})
run(dispatch, "upper", []string{"hello", "gopher"})
run(dispatch, "help", nil)
run(dispatch, "unknown", nil)
}
Expected output:
add([1 2 3 4]) = 10
mul([2 3 5]) = 30
HELLO GOPHER
commands: add, mul, upper, help
unknown command "unknown". Try 'help'
Checkpoint: Add a “sub” command that subtracts all args from the first. The dispatcher doesn’t need to change — just add a new key. This is the open/closed principle in Go.
Lab 6 (Final): Word Frequency Counter
What you’ll practise: Combining maps, slices, sorting, and string processing into a complete top-N word frequency reporter.
Task:
Write day-06/main.go that parses multi-line text into words, builds a frequency table, sorts by count descending, and prints a formatted top-10 table.
Steps:
- Lowercase and strip punctuation from each word using
strings.Map - Build a
map[string]intfrequency table - Collect map entries into
[]Entry{Word, Count}and sort by count descending - Print the top 10 in a formatted table with rank, word, count, and a bar chart
package main
import (
"fmt"
"sort"
"strings"
)
const text = `To be or not to be that is the question
Whether tis nobler in the mind to suffer
The slings and arrows of outrageous fortune
Or to take arms against a sea of troubles`
type Entry struct {
Word string
Count int
}
func main() {
// Normalise: lowercase, strip non-letter characters
clean := strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r == ' ' || r == '\n' {
return r
}
if r >= 'A' && r <= 'Z' {
return r + 32 // tolower
}
return -1 // drop
}, text)
// Build frequency map
freq := make(map[string]int)
for _, w := range strings.Fields(clean) {
if w != "" {
freq[w]++
}
}
// Sort entries by count descending, then alphabetically
entries := make([]Entry, 0, len(freq))
for w, n := range freq {
entries = append(entries, Entry{w, n})
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].Count != entries[j].Count {
return entries[i].Count > entries[j].Count
}
return entries[i].Word < entries[j].Word
})
// Print top 10
top := 10
if len(entries) < top {
top = len(entries)
}
fmt.Printf("%-4s %-12s %5s %s\n", "Rank", "Word", "Count", "Bar")
fmt.Println(strings.Repeat("-", 40))
for i, e := range entries[:top] {
bar := strings.Repeat("*", e.Count)
fmt.Printf("%-4d %-12s %5d %s\n", i+1, e.Word, e.Count, bar)
}
}
Expected output:
Rank Word Count Bar
----------------------------------------
1 to 5 *****
2 the 3 ***
3 or 2 **
4 be 2 **
5 of 2 **
...
Checkpoint: go run . prints the table. The word “to” ranks first (appears 5 times). Sorting is stable: ties are broken alphabetically. Run go vet . — no warnings.
Day Project: Word Frequency Counter
Extend the Day 05 string statistics tool:
- Parse a multi-line text into words (lowercase, strip punctuation with
strings.Map) - Build a
map[string]intfrequency table - Find the top-N most frequent words (collect map entries, sort by count descending)
- Print a formatted table
type Entry struct {
Word string
Count int
}
Extension ideas: read from a file path passed as os.Args[1]; add a --top N flag.
Official Documentation
fmt— formatted output (Printf, Println)strings—Map,ToLower,Fieldsfor text processingsort— sorting slices for top-N resultsos—os.Argsfor file path arguments- Language Spec: Map types — map declaration and usage
- Language Spec: Method sets — value vs pointer method sets
- Language Spec: Struct types — embedding
- Effective Go: Embedding — composition via embedding
- Go Tour: Maps — maps tour