Exercises — Module 7: Packages and Modules¶
Work through exercises in order — they're designed to build on each other. Attempt each problem genuinely before looking at the solution. Seeing the solution first might feel like progress, but it isn't.
Instructions¶
- Attempt first. Spend at least the estimated time on each problem before checking hints or solutions.
- Write your work. Don't just think through it — actually create the directories and files.
- Check your answer against the acceptance criteria, not just the solution code.
- Score yourself honestly in the Scoring Log at the bottom.
- If you're stuck after a genuine effort, use the hints one at a time — not all at once.
Difficulty Legend¶
| Symbol | Difficulty | Expected Time | Points |
|---|---|---|---|
| 🟢 Easy | Recall and basic application | 5–10 min | 1 pt |
| 🟡 Medium | Requires combining 2+ concepts | 15–25 min | 2 pts |
| 🔴 Hard | Multi-step, requires real problem-solving | 30–60 min | 3 pts |
| ⭐ Expert | Open-ended; more than one good answer | 60+ min | 5 pts |
Exercise 1: Single-Package Module [🟢 Easy] [1 pt]¶
Context¶
The most fundamental Go project structure is a module containing a single library package and one package main that uses it. This exercise practices go mod init, package declaration, and the exported/unexported distinction.
Task¶
Create a new Go module github.com/example/calc with the following structure:
calculator.go must define:
- Exported functions: Add(a, b int) int, Sub(a, b int) int, Mul(a, b int) int, Div(a, b float64) float64
- An unexported helper validate(a, b float64) bool that returns false if b is zero (used by Div)
main.go must import github.com/example/calc/calculator and call all four functions, printing the results.
Requirements¶
-
go.modexists withmodule github.com/example/calcandgo 1.22 -
Add,Sub,Mul,Divare exported (uppercase) -
validateis unexported (lowercase) and is only used within thecalculatorpackage -
main.gocannot referencevalidatedirectly (compile error if attempted) -
go run ./cmd/calc/produces correct output
Hints¶
Hint 1 (try without this first)
Create the structure with: Then create the source files. `go mod init` creates `go.mod` for you.Hint 2 (if structure is right but compile fails)
The import path in `main.go` must use the full module path, not just the directory name: You then use the package with its name: `calculator.Add(3, 4)`.Expected Output / Acceptance Criteria¶
Solution¶
Show Solution (attempt first!)
// calculator/calculator.go
package calculator
// validate returns false if b is zero, preventing division by zero.
func validate(a, b float64) bool {
return b != 0
}
// Add returns the sum of a and b.
func Add(a, b int) int { return a + b }
// Sub returns the difference a - b.
func Sub(a, b int) int { return a - b }
// Mul returns the product of a and b.
func Mul(a, b int) int { return a * b }
// Div returns a / b. Returns 0 if b is zero.
func Div(a, b float64) float64 {
if !validate(a, b) {
return 0
}
return a / b
}
// cmd/calc/main.go
package main
import (
"fmt"
"github.com/example/calc/calculator"
)
func main() {
fmt.Printf("Add(10, 3) = %d\n", calculator.Add(10, 3))
fmt.Printf("Sub(10, 3) = %d\n", calculator.Sub(10, 3))
fmt.Printf("Mul(10, 3) = %d\n", calculator.Mul(10, 3))
fmt.Printf("Div(10, 3) = %g\n", calculator.Div(10, 3))
}
Exercise 2: Export Rules — Reading a Package's API [🟢 Easy] [1 pt]¶
Context¶
A core skill in Go is being able to read any package and immediately know its public API just from capitalization — without reading the implementation. This exercise tests that skill.
Task¶
Given the following Go file (do not run it — read it), list: 1. Every identifier that is exported (visible to other packages) 2. Every identifier that is unexported (package-private only) 3. What the public API of this package is — what callers can do with it
package cache
import (
"sync"
"time"
)
const defaultTTL = 60 // seconds
var ErrNotFound = errors.New("key not found")
type entry struct {
value interface{}
expires time.Time
}
type Cache struct {
mu sync.Mutex
data map[string]entry
MaxSize int
}
func New(maxSize int) *Cache {
return &Cache{
data: make(map[string]entry),
MaxSize: maxSize,
}
}
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = entry{value: value, expires: time.Now().Add(ttl)}
}
func (c *Cache) Get(key string) (interface{}, error) {
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.data[key]
if !ok || time.Now().After(e.expires) {
return nil, ErrNotFound
}
return e.value, nil
}
func (c *Cache) evict() {
// removes expired entries — internal housekeeping
}
Requirements¶
- Correctly list all exported identifiers (at least 5)
- Correctly list all unexported identifiers (at least 4)
- Describe the public API in one sentence
Expected Output / Acceptance Criteria¶
Exported (public):
- ErrNotFound (var)
- Cache (type)
- Cache.MaxSize (struct field)
- New (function)
- Cache.Set (method)
- Cache.Get (method)
Unexported (package-private):
- defaultTTL (const)
- entry (type)
- entry.value (field)
- entry.expires (field)
- Cache.mu (field)
- Cache.data (field)
- Cache.evict (method)
Public API summary: The cache package provides a thread-safe, TTL-based in-memory cache; callers create one with New, store values with Set, retrieve them with Get (which returns ErrNotFound for missing/expired keys), and may check MaxSize.
Solution¶
Show Solution
The analysis above is the complete solution. The key observation: `Cache` is exported (callers can create and use it), but `mu` and `data` are unexported (callers cannot directly manipulate the lock or the underlying map — they must use `Set` and `Get`). This prevents callers from corrupting the cache's invariants. The `entry` type being unexported means callers never see the internal representation — the cache can change its storage format without breaking callers. **Why `ErrNotFound` is exported:** Callers need to check for it with `errors.Is(err, cache.ErrNotFound)`. If it were unexported, callers could not distinguish "not found" from any other error. Sentinel error values must be exported to be checkable. **Common wrong answer:** Listing `MaxSize` as unexported because it's a field. Fields follow the same rule — uppercase first letter = exported. `MaxSize` starts with `M` (uppercase), so it is exported and callers can read and set it.Exercise 3: Creating a go.mod with a Real Dependency [🟡 Medium] [2 pts]¶
Context¶
The most common day-to-day module workflow is: create a module, add a dependency, write code that uses it, and verify the go.mod and go.sum are correct. This exercise practices that full cycle.
Task¶
Create a Go module github.com/example/logger that:
1. Initializes the module with go mod init
2. Adds github.com/rs/zerolog as a dependency using go get
3. Creates a cmd/log/main.go that uses zerolog to log one Info message and one Error message with at least one structured field each
4. Runs go mod tidy and commits (or verifies) that go.mod and go.sum are consistent
Requirements¶
-
go.modcontainsmodule github.com/example/loggerand arequireentry forgithub.com/rs/zerolog -
go.sumexists and is non-empty - The program compiles and runs with
go run ./cmd/log/ - The Info message includes at least one structured key-value field (e.g.,
Str("component", "main")) - The Error message includes at least one structured field and a message string
- Running
go mod tidyafter the code is written produces no changes togo.modorgo.sum
Hints¶
Hint 1 (workflow hint)
Hint 2 (zerolog usage hint)
import (
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
// Configure human-readable console output:
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
// Info with a field:
log.Info().Str("component", "main").Msg("started")
// Error with a field:
log.Error().Str("op", "startup").Msg("something went wrong")
Expected Output / Acceptance Criteria¶
Output will resemble (exact format depends on zerolog version and console writer configuration):
The key criterion is that go.mod has a require entry for zerolog and go.sum is present and non-empty.
Solution¶
Show Solution
mkdir logger && cd logger
go mod init github.com/example/logger
mkdir -p cmd/log
go get github.com/rs/zerolog@latest
// cmd/log/main.go
package main
import (
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// Human-readable output for local development
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
// Info log with a structured field
log.Info().
Str("component", "main").
Int("pid", os.Getpid()).
Msg("application started")
// Error log with a structured field (simulated error)
log.Error().
Str("op", "database_connect").
Str("host", "localhost").
Msg("connection refused")
}
Exercise 4: internal/ Package Enforcement [🟡 Medium] [2 pts]¶
Context¶
The internal/ directory is Go's first-class mechanism for package-level encapsulation. The toolchain enforces it — it is not just a convention. This exercise demonstrates that the compiler actually refuses to build code that violates the internal/ boundary.
Task¶
Create a module github.com/example/internal-demo with this structure:
internal-demo/
go.mod
internal/
secrets/
secrets.go — package secrets; exports SecretKey string
cmd/app/
main.go — LEGAL: same module, imports internal/secrets
cmd/cheat/
main.go — attempt to import from a DIFFERENT module (simulate this)
- Write
internal/secrets/secrets.gowith an exportedvar SecretKey = "s3cr3t" - Write
cmd/app/main.gothat successfully imports and printssecrets.SecretKey - Run
go build ./...and confirm it succeeds - Create a separate module
github.com/example/outsiderin a sibling directory that tries to importgithub.com/example/internal-demo/internal/secrets - Run
go build ./...inside the outsider module and confirm the compiler error
Requirements¶
- The in-module import (
cmd/app) compiles successfully - The cross-module import attempt produces a compile-time error mentioning "use of internal package"
- Record the exact error message in your notes
Hints¶
Hint 1 (structure hint)
Hint 2 (what the error looks like)
When you run `go build` in the `outsider` module, you should see something like: or if the package is downloaded:Expected Output / Acceptance Criteria¶
# Inside internal-demo:
$ go build ./...
# (success, no output)
# Inside outsider (after attempting to import internal/secrets):
$ go build ./...
main.go:5:2: use of internal package \
github.com/example/internal-demo/internal/secrets not allowed
Solution¶
Show Solution
// internal-demo/internal/secrets/secrets.go
package secrets
// SecretKey is the application secret. This package is internal —
// only code within github.com/example/internal-demo can import it.
var SecretKey = "s3cr3t"
Exercise 5: go mod tidy and Dependency Hygiene [🟡 Medium] [2 pts]¶
Context¶
go mod tidy is the command that keeps go.mod and go.sum synchronized with what is actually imported. This exercise demonstrates what happens when go.mod and your imports diverge — and how go mod tidy fixes it.
Task¶
Starting from Exercise 3's logger module (or create a fresh module):
1. Add a second dependency with go get github.com/google/uuid@latest
2. Write code that uses uuid to generate and print a UUID
3. Then remove the uuid import from your code (but do not run go mod tidy yet)
4. Observe that go.mod still lists uuid as a requirement even though no code imports it
5. Run go mod tidy and observe that uuid is removed from go.mod and go.sum
6. Now add an import for github.com/fatih/color in your code WITHOUT running go get first
7. Observe that go build fails with "no required module provides..."
8. Run go mod tidy again and observe that it adds the missing dependency
Requirements¶
- Demonstrate the "stale dependency" case (go.mod has more than code imports)
- Demonstrate the "missing dependency" case (code imports more than go.mod declares)
-
go mod tidyfixes both cases - Record the exact error message for the missing dependency case
Hints¶
Hint 1 (uuid usage)
Expected Output / Acceptance Criteria¶
# After removing uuid from code but NOT running tidy:
$ cat go.mod
# still shows: require github.com/google/uuid v...
# After running go mod tidy:
$ go mod tidy
$ cat go.mod
# uuid is gone
# After adding fatih/color import but NOT running go get:
$ go build ./...
main.go:5:2: no required module provides package github.com/fatih/color; \
to add it: go get github.com/fatih/color
# After running go mod tidy:
$ go mod tidy
go: finding module for package github.com/fatih/color
go: added github.com/fatih/color v1.16.0
Solution¶
Show Solution
# Step 1: add uuid
go get github.com/google/uuid@latest
# Step 2: use it in code (write uuid import into main.go)
# Step 3: remove the uuid import from main.go
# Step 4: observe go.mod still has uuid
cat go.mod # uuid is still listed
# Step 5: tidy removes it
go mod tidy
cat go.mod # uuid is gone
# Step 6: add fatih/color import to code (do NOT run go get)
# edit main.go to add: import "github.com/fatih/color"
# and use color.Green("hello")
# Step 7: build fails
go build ./...
# main.go:5:2: no required module provides package github.com/fatih/color
# Step 8: tidy adds it
go mod tidy
go build ./... # now succeeds
Exercise 6: Multi-Package Layout with cmd/ and internal/ [🔴 Hard] [3 pts]¶
Context¶
Real Go projects use the cmd/ + internal/ layout to separate binaries from implementation and enforce encapsulation. This exercise builds a realistic two-binary module from scratch.
Task¶
Create a module github.com/example/notes with two binaries and shared internal packages:
notes/
go.mod
cmd/
add/
main.go — reads args; calls internal/store to add a note
list/
main.go — calls internal/store to list all notes; prints them
internal/
store/
store.go — in-memory []Note store; exported: Note type, Add(), List()
format/
format.go — unexported helpers + exported: FormatNote(n Note) string
Requirements for internal/store:
- type Note struct { ID int; Text string; CreatedAt time.Time }
- func Add(text string) Note — appends a new note with auto-incremented ID and current time
- func List() []Note — returns all notes
Requirements for internal/format:
- func FormatNote(n store.Note) string — returns "[1] Hello world (2024-01-15 12:00)"
cmd/add/main.go — reads os.Args[1] as the note text, calls store.Add, prints the formatted note.
cmd/list/main.go — calls store.List, formats and prints each note with format.FormatNote.
Requirements¶
- Both binaries build with
go build ./cmd/add/ && go build ./cmd/list/ -
cmd/add/main.goimports bothinternal/storeandinternal/format -
internal/formatimportsinternal/store(packages within the same module can import each other's internal packages) - The in-memory store is shared via package-level state within a single process
-
go run ./cmd/add/ "Buy milk"prints the formatted new note -
go run ./cmd/list/prints all previously added notes (note: since the store is in-memory, each invocation starts fresh — that is acceptable)
Hints¶
Hint 1 (package-level state)
Package-level vars are shared across all callers within the same process. Since each `go run` is a fresh process, the store starts empty each time — this is expected for an in-memory store without persistence.Hint 2 (import path between internal packages)
`internal/format` can import `internal/store` because both are within the same module:Hint 3 (format function)
Go's time format uses the reference time `Mon Jan 2 15:04:05 MST 2006` — `2006-01-02 15:04` is year-month-day hour:minute.Expected Output / Acceptance Criteria¶
$ go run ./cmd/add/ "Buy milk"
[1] Buy milk (2024-06-09 12:00)
$ go run ./cmd/add/ "Call dentist"
[1] Call dentist (2024-06-09 12:01)
$ go run ./cmd/list/
(empty — fresh process, no notes)
Solution¶
Show Solution (attempt first!)
// internal/store/store.go
package store
import "time"
// Note represents a single text note.
type Note struct {
ID int
Text string
CreatedAt time.Time
}
// package-level state: shared within one process
var (
notes []Note
nextID = 1
)
// Add creates and stores a new note with the given text.
func Add(text string) Note {
n := Note{ID: nextID, Text: text, CreatedAt: time.Now()}
notes = append(notes, n)
nextID++
return n
}
// List returns all notes in creation order.
func List() []Note {
return notes
}
// internal/format/format.go
package format
import (
"fmt"
"github.com/example/notes/internal/store"
)
// FormatNote returns a human-readable string for a note.
func FormatNote(n store.Note) string {
return fmt.Sprintf("[%d] %s (%s)",
n.ID, n.Text, n.CreatedAt.Format("2006-01-02 15:04"))
}
Exercise 7: Multi-Module Workspace [⭐ Expert] [5 pts]¶
Context¶
Go workspaces (go.work) solve the multi-module local development problem: you are actively developing both a library (mylib) and an application (myapp) that depends on it. Without a workspace, you'd need to publish every mylib change before myapp can use it.
Task¶
Create two sibling modules in a parent directory, then link them with a workspace:
dev/
go.work — created by go work init
mylib/
go.mod — module github.com/example/mylib
greeter.go — package mylib; exports Greet(name string) string
myapp/
go.mod — module github.com/example/myapp; requires mylib v0.1.0
main.go — imports mylib and calls mylib.Greet
Steps:
1. Create mylib with go mod init github.com/example/mylib and a Greet function
2. Create myapp with go mod init github.com/example/myapp — add require github.com/example/mylib v0.1.0 to its go.mod manually (the module does not exist on any proxy, which is fine for workspace use)
3. Create the workspace: go work init ./mylib ./myapp
4. Build myapp and confirm it uses the local mylib (not a proxy version)
5. Modify mylib's Greet function to return a different greeting
6. Rebuild myapp — confirm it sees the change immediately without any go get or go mod tidy
7. Demonstrate that GOWORK=off go build ./... in myapp fails (cannot find the module)
Requirements¶
-
go.workexists indev/withusedirectives for both modules -
myapp/go.moddoes NOT have areplacedirective -
go buildinmyapp/succeeds with the workspace active - Modifying
mylib/greeter.gois immediately visible inmyappwithout any other command -
GOWORK=off go build ./...insidemyapp/fails with a "no required module provides" or "cannot find module" error - A
go.workis not insidemylib/ormyapp/— it is in the parentdev/directory
Hints¶
Hint 1 (go.mod for myapp without a published mylib)
You can write a `require` for a module that doesn't exist on any proxy. With the workspace active, the build system uses the local path from `go.work` instead. `go.mod` still needs the `require` line for `go build` to know what the import path resolves to.Hint 2 (workspace commands)
Hint 3 (verifying workspace vs non-workspace behavior)
Expected Output / Acceptance Criteria¶
# Initial build:
$ cd dev/myapp && go build -o myapp . && ./myapp
Hello, World!
# After modifying mylib/greeter.go to return "Greetings, World!":
$ go build -o myapp . && ./myapp
Greetings, World! ← change visible immediately, no go get needed
# Without workspace:
$ GOWORK=off go build .
main.go:5:2: no required module provides \
package github.com/example/mylib; to add it:
go get github.com/example/mylib
Solution¶
Show Solution (attempt first!)
// dev/mylib/greeter.go
package mylib
import "fmt"
// Greet returns a greeting string for the given name.
func Greet(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
// dev/myapp/main.go
package main
import (
"fmt"
"github.com/example/mylib"
)
func main() {
fmt.Println(mylib.Greet("World"))
}
# dev/myapp/go.mod (written manually or via go mod init + manual require)
module github.com/example/myapp
go 1.22
require github.com/example/mylib v0.1.0
cd dev/mylib && go mod init github.com/example/mylib
cd ../myapp && go mod init github.com/example/myapp
# Add require manually to go.mod
cd .. # back to dev/
go work init ./mylib ./myapp
# Build and run:
cd myapp
go build -o myapp . && ./myapp
# Hello, World!
# Modify mylib/greeter.go: change "Hello" to "Greetings"
# Rebuild immediately:
go build -o myapp . && ./myapp
# Greetings, World!
# Disable workspace:
GOWORK=off go build .
# error: no required module provides package github.com/example/mylib
Scoring Log¶
Record your performance honestly. Include the date and whether you used hints.
| Exercise | Date | Score | Used Hints? | Notes |
|---|---|---|---|---|
| Exercise 1 — Single-Package Module | — | —/1 | — | — |
| Exercise 2 — Export Rules Reading | — | —/1 | — | — |
| Exercise 3 — go.mod with Real Dependency | — | —/2 | — | — |
| Exercise 4 — internal/ Enforcement | — | —/2 | — | — |
| Exercise 5 — go mod tidy Hygiene | — | —/2 | — | — |
| Exercise 6 — Multi-Package Layout | — | —/3 | — | — |
| Exercise 7 — Multi-Module Workspace | — | —/5 | — | — |
| Total | —/16 |
Passing threshold: 10/16 (63%). Aim for 13/16 (81%) before taking the test.