Exercises — Module 6: Methods and Interfaces¶
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 run code mentally — actually write or type your attempt.
- 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: Implementing fmt.Stringer [🟢 Easy] [1 pt]¶
Context¶
fmt.Stringer is the most commonly implemented interface in Go. Any type that has a String() string method will have that method called automatically by fmt.Println, fmt.Printf("%v", ...), and fmt.Sprintf. Implementing it makes your types first-class citizens in any code that uses fmt.
Task¶
Define a named type Temperature as a float64. Implement fmt.Stringer on it so that it prints in the format "23.5°C" (one decimal place, degree symbol, C). Call fmt.Println with at least three Temperature values to confirm the output uses your String() method.
Requirements¶
-
Temperatureis a named type based onfloat64, not a struct - The
String()method uses a value receiver (not pointer) -
fmt.Printlnoutput matches the"23.5°C"format exactly (one decimal,°C) - The program compiles and runs without errors
Hints¶
Hint 1 (try without this first)
The `fmt.Stringer` interface is: Your `String()` method must have exactly this signature. Use `fmt.Sprintf("%.1f°C", float64(t))` to format the value — cast to `float64` first to avoid an infinite recursion (calling `fmt.Sprintf` on a `Temperature` would call `String()` again).Hint 2 (only if Hint 1 wasn't enough)
The degree symbol `°` is a Unicode character (U+00B0). You can include it directly in a string literal: `"%.1f°C"`. Alternatively, use the Unicode escape `"°"`. Both work in Go string literals.Expected Output / Acceptance Criteria¶
Solution¶
Show Solution (attempt first!)
package main
import "fmt"
// Temperature represents a temperature in Celsius
type Temperature float64
// String satisfies fmt.Stringer — called automatically by fmt verbs
func (t Temperature) String() string {
return fmt.Sprintf("%.1f°C", float64(t))
// Cast to float64 to avoid infinite recursion:
// if we passed t directly, fmt.Sprintf would call t.String() again
}
func main() {
fmt.Println(Temperature(0)) // 0.0°C
fmt.Println(Temperature(100)) // 100.0°C
fmt.Println(Temperature(-40)) // -40.0°C
// Also works in other fmt contexts:
t := Temperature(23.5)
fmt.Printf("Current temperature: %v\n", t) // Current temperature: 23.5°C
fmt.Printf("Current temperature: %s\n", t) // Current temperature: 23.5°C
}
Exercise 2: Pointer Receiver and Interface Satisfaction [🟢 Easy] [1 pt]¶
Context¶
The method set of T and *T differ: T only includes value-receiver methods, while *T includes both. This affects interface satisfaction. This exercise isolates that rule so you can see it clearly.
Task¶
Define a Counter struct with a single int field n. Add two methods:
- Value() int with a value receiver — returns the current count
- Increment() with a pointer receiver — increments the count by 1
Define an interface Incrementer with both methods. Demonstrate:
1. That *Counter satisfies Incrementer (assign &Counter{} to an Incrementer variable)
2. That Counter (a value) does NOT satisfy Incrementer (show the compile-time error in a comment)
3. That calling Increment() through the Incrementer interface correctly modifies the original
Requirements¶
-
Value()has a value receiver;Increment()has a pointer receiver - Assignment
var inc Incrementer = &Counter{}compiles - A comment shows what
var inc Incrementer = Counter{}would produce (the error message) - After calling
inc.Increment()three times,inc.Value()returns 3
Hints¶
Hint 1
The compile error for `var inc Incrementer = Counter{}` will be something like: `cannot use Counter{} (type Counter) as type Incrementer: Counter does not implement Incrementer (Increment method has pointer receiver)` You don't have to trigger this error — just include it as a comment.Expected Output / Acceptance Criteria¶
Solution¶
Show Solution
package main
import "fmt"
type Counter struct{ n int }
// Value receiver: Counter satisfies this method in its method set
func (c Counter) Value() int { return c.n }
// Pointer receiver: only *Counter satisfies this method
func (c *Counter) Increment() { c.n++ }
// Incrementer requires both Value and Increment
type Incrementer interface {
Value() int
Increment()
}
func main() {
// *Counter implements Incrementer — has both Value() and Increment()
var inc Incrementer = &Counter{}
// Counter (value) does NOT implement Incrementer:
// var inc2 Incrementer = Counter{}
// compile error: Counter does not implement Incrementer
// (Increment method has pointer receiver)
inc.Increment()
inc.Increment()
inc.Increment()
fmt.Printf("after 3 increments: %d\n", inc.Value()) // 3
}
Exercise 3: Interface Composition with io Interfaces [🟡 Medium] [2 pts]¶
Context¶
Go's io package achieves enormous flexibility through small, composable interfaces. io.Reader, io.Writer, and io.ReadWriter (their composition) are used everywhere. This exercise builds a function that accepts the most general interface it needs — demonstrating why small interfaces and composition are powerful.
Task¶
Write a function copyAndCount(dst io.Writer, src io.Reader) (int64, error) that copies all bytes from src to dst using io.Copy, and returns the number of bytes copied and any error. Then write a main that:
1. Copies from a strings.NewReader("Hello, interfaces!") to a bytes.Buffer
2. Prints the number of bytes copied and the buffer's contents
Neither strings.Reader nor bytes.Buffer appears in the function signature — only the interfaces do.
Requirements¶
- Function signature is exactly
func copyAndCount(dst io.Writer, src io.Reader) (int64, error) - Uses
io.Copyinternally (do not write your own byte loop) -
maincalls the function and prints both the byte count and the copied content - Imports:
"bytes","fmt","io","strings"
Hints¶
Hint 1
`io.Copy(dst, src)` returns `(int64, error)` — it copies from `src` to `dst` and returns the total bytes written. Your function can simply return `io.Copy(dst, src)`.Hint 2
`strings.NewReader(s)` returns a `*strings.Reader` which implements `io.Reader`. `bytes.Buffer` (use `var buf bytes.Buffer` or `new(bytes.Buffer)`) implements `io.Writer`. Both can be passed directly to your function because they satisfy the interfaces.Expected Output / Acceptance Criteria¶
Solution¶
Show Solution
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
// copyAndCount accepts the minimal interfaces it needs — not *os.File or bytes.Buffer
func copyAndCount(dst io.Writer, src io.Reader) (int64, error) {
return io.Copy(dst, src)
}
func main() {
src := strings.NewReader("Hello, interfaces!") // *strings.Reader implements io.Reader
var dst bytes.Buffer // bytes.Buffer implements io.Writer
n, err := copyAndCount(&dst, src)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("copied %d bytes\n", n)
fmt.Printf("content: %s\n", dst.String())
}
Exercise 4: Type Switch on Interface Values [🟡 Medium] [2 pts]¶
Context¶
A type switch lets you dispatch on the concrete type inside an interface value. This is essential for functions that accept any (or a broad interface) and need to behave differently based on the actual type. This exercise practices the full type switch syntax including an interface type case.
Task¶
Write a function formatValue(v any) string that returns a formatted string based on the concrete type of v:
- nil → "<nil>"
- int → "int: <value>"
- float64 → "float64: <value>" (use %g format)
- string → "string: <value>" (use %q format)
- bool → "bool: <value>"
- fmt.Stringer → "Stringer: <value.String()>" (any type that implements Stringer)
- anything else → "other: <Go type name>"
Call formatValue with at least 7 different values (one per case, including a type that satisfies fmt.Stringer) and print the results.
Requirements¶
- Uses a type switch
switch t := v.(type) { ... } - The
fmt.Stringercase is an interface type case (not a concrete type) - All 7 case types are covered
- A custom type implementing
fmt.Stringeris used to exercise that case
Hints¶
Hint 1
In a type switch, a case can be an interface type: `case fmt.Stringer:`. Within that case, `t` will be typed as `fmt.Stringer`. Order matters: the `fmt.Stringer` case must come after the concrete type cases (int, string, etc.) that also happen to satisfy Stringer — or those concrete types will match the Stringer case first. Put the specific concrete types before the interface case.Hint 2
For a custom type that satisfies `fmt.Stringer`, you can reuse the `Temperature` type from Exercise 1, or define a new simple type with a `String() string` method.Expected Output / Acceptance Criteria¶
Solution¶
Show Solution
package main
import "fmt"
// Reuse Temperature from Exercise 1
type Temperature float64
func (t Temperature) String() string {
return fmt.Sprintf("%.1f°C", float64(t))
}
func formatValue(v any) string {
switch t := v.(type) {
case nil:
return "<nil>"
case int:
return fmt.Sprintf("int: %d", t) // t is typed int here
case float64:
return fmt.Sprintf("float64: %g", t) // t is typed float64 here
case string:
return fmt.Sprintf("string: %q", t) // t is typed string here
case bool:
return fmt.Sprintf("bool: %v", t)
case fmt.Stringer:
return fmt.Sprintf("Stringer: %s", t.String()) // t is typed fmt.Stringer here
default:
return fmt.Sprintf("other: %T", t) // %T prints the Go type name
}
}
func main() {
values := []any{
nil,
42,
3.14,
"hello",
true,
Temperature(100),
[]int{1, 2, 3},
}
for _, v := range values {
fmt.Println(formatValue(v))
}
}
Exercise 5: Sorting a Custom Type with sort.Interface [🟡 Medium] [2 pts]¶
Context¶
sort.Interface is the canonical example of a three-method interface that makes any collection sortable. Before sort.Slice existed (Go 1.8), this was the only way to sort custom types. Understanding it deeply illustrates how interface-based extensibility works: the sort package knows nothing about your type, yet it can sort it.
Task¶
Define a Product struct with fields Name string and Price float64. Create a named type ByPrice as []Product. Implement sort.Interface on ByPrice (Len, Less, Swap). In main, create a slice of at least 5 products, sort it by price ascending, and print the results. Then sort the same slice by price descending using sort.Reverse and print again.
Requirements¶
-
ByPriceimplements all three methods:Len,Less,Swap - Products are sorted ascending by price first, descending second
-
sort.Reverse(ByPrice(products))is used for the descending sort — do not re-implement Less - Output clearly labels which sort is which
Hints¶
Hint 1
`sort.Reverse(data sort.Interface)` returns a `sort.Interface` that sorts in reverse order — it wraps your `ByPrice` and inverts the `Less` method. Usage: `sort.Sort(sort.Reverse(ByPrice(products)))`. You do not need a separate `ByPriceDesc` type.Hint 2
`Len` returns `len(b)`, `Swap` does `b[i], b[j] = b[j], b[i]`, `Less` returns `b[i].Price < b[j].Price`. These are the standard implementations.Expected Output / Acceptance Criteria¶
--- Ascending by price ---
Pen $1.99
Notebook $4.99
Stapler $8.50
Backpack $29.99
Laptop $999.00
--- Descending by price ---
Laptop $999.00
Backpack $29.99
Stapler $8.50
Notebook $4.99
Pen $1.99
Solution¶
Show Solution
package main
import (
"fmt"
"sort"
)
type Product struct {
Name string
Price float64
}
// ByPrice implements sort.Interface for []Product sorted by Price
type ByPrice []Product
func (p ByPrice) Len() int { return len(p) }
func (p ByPrice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p ByPrice) Less(i, j int) bool { return p[i].Price < p[j].Price }
func main() {
products := []Product{
{"Laptop", 999.00},
{"Pen", 1.99},
{"Backpack", 29.99},
{"Notebook", 4.99},
{"Stapler", 8.50},
}
sort.Sort(ByPrice(products))
fmt.Println("--- Ascending by price ---")
for _, p := range products {
fmt.Printf("%-10s $%.2f\n", p.Name, p.Price)
}
sort.Sort(sort.Reverse(ByPrice(products)))
fmt.Println("\n--- Descending by price ---")
for _, p := range products {
fmt.Printf("%-10s $%.2f\n", p.Name, p.Price)
}
}
Exercise 6: Designing a Domain Interface [🔴 Hard] [3 pts]¶
Context¶
Good Go interface design means defining the interface at the consumer, keeping it small, and letting multiple unrelated types satisfy it implicitly. This exercise gives you practice designing an interface from scratch and satisfying it with two independent types.
Task¶
Design a small notification system:
- Define an interface
Notifierwith a single methodNotify(message string) error - Implement
EmailNotifier(a struct withToAddress string) whoseNotifymethod prints"[EMAIL to <addr>]: <message>"and returns nil - Implement
SlackNotifier(a struct withChannel string) whoseNotifymethod prints"[SLACK #<channel>]: <message>"and returns nil - Write a function
notifyAll(notifiers []Notifier, message string) []errorthat callsNotifyon each notifier, collects non-nil errors into a slice, and returns it - In
main, create a mixed slice ofEmailNotifierandSlackNotifier, callnotifyAll, and print any errors (there should be none)
Requirements¶
-
Notifierinterface defined in the same file/package as the function that uses it (consumer-side) - Both types satisfy
Notifierimplicitly — noimplementskeyword -
notifyAllaccepts[]Notifier, not[]EmailNotifieror[]SlackNotifier -
notifyAllcollects errors and returns them — does not abort on first error - The output shows both email and Slack notifications interleaved
Hints¶
Hint 1 (structural hint)
Since `Notify` returns `nil` (no error) in both implementations, `notifyAll` will return an empty slice. To make the function more robust, collect non-nil errors: `if err := n.Notify(message); err != nil { errs = append(errs, err) }`.Hint 2 (the interface assignment)
To create a `[]Notifier` with mixed types, you cannot use a typed slice literal directly. Either declare the slice as `[]Notifier` and append elements, or use a composite literal: `[]Notifier{EmailNotifier{...}, &SlackNotifier{...}}`. If your Notify method uses a pointer receiver, you'll need `&SlackNotifier{...}`.Hint 3 (near-solution hint — only if truly stuck)
Expected Output / Acceptance Criteria¶
[EMAIL to alice@example.com]: system maintenance at midnight
[EMAIL to bob@example.com]: system maintenance at midnight
[SLACK #alerts]: system maintenance at midnight
errors: 0
Solution¶
Show Solution
package main
import "fmt"
// Notifier is defined at the consumer — the notification service.
// EmailNotifier and SlackNotifier don't need to know about this interface.
type Notifier interface {
Notify(message string) error
}
// EmailNotifier sends notifications via email (simulated here as a print)
type EmailNotifier struct {
ToAddress string
}
func (e EmailNotifier) Notify(message string) error {
fmt.Printf("[EMAIL to %s]: %s\n", e.ToAddress, message)
return nil
}
// SlackNotifier sends notifications to a Slack channel (simulated)
type SlackNotifier struct {
Channel string
}
func (s SlackNotifier) Notify(message string) error {
fmt.Printf("[SLACK #%s]: %s\n", s.Channel, message)
return nil
}
// notifyAll sends message to all notifiers, collecting errors
func notifyAll(notifiers []Notifier, message string) []error {
var errs []error
for _, n := range notifiers {
if err := n.Notify(message); err != nil {
errs = append(errs, err)
}
}
return errs
}
func main() {
notifiers := []Notifier{
EmailNotifier{ToAddress: "alice@example.com"},
EmailNotifier{ToAddress: "bob@example.com"},
SlackNotifier{Channel: "alerts"},
}
errs := notifyAll(notifiers, "system maintenance at midnight")
fmt.Printf("errors: %d\n", len(errs))
}
Exercise 7: The Typed-Nil Bug Hunt [🔴 Hard] [3 pts]¶
Context¶
The typed-nil interface bug is subtle and common. This exercise presents buggy code and asks you to find and fix it, explain why it happens at the interface value level, and write a test that would catch it.
Task¶
The following function is broken. Find the bug, fix it, explain why the original was wrong at the (type, value) pair level, and write a test (using if err != nil) that demonstrates the fix works:
type AppError struct {
Code int
Message string
}
func (e *AppError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}
// validate returns an error if n is negative; nil otherwise.
func validate(n int) error {
var err *AppError
if n < 0 {
err = &AppError{Code: 400, Message: "negative value not allowed"}
}
return err // BUG IS HERE
}
Requirements¶
- Correctly identify the line with the bug and explain why it is a bug
- Provide a corrected version of
validate - Explain the
(type, value)pair state of the return value whenn >= 0in the buggy version - Write a
mainthat callsvalidate(5)andvalidate(-1)and shows that the fixed version returnsnilforn=5and non-nil forn=-1
Hints¶
Hint 1 (what to look for)
The bug is on the `return err` line. When `n >= 0`, `err` is a `*AppError` variable holding a nil pointer. When you return `err` (a typed nil) as an `error` interface, what are the type and value fields of the resulting interface?Hint 2 (the fix)
The fix requires changing the return path when there is no error. Instead of declaring `var err *AppError` and returning it unconditionally, return `nil` (the untyped nil) directly when there is no error:Expected Output / Acceptance Criteria¶
validate(5): err == nil? true (correct)
validate(-1): err == nil? false (correct, error: error 400: negative value not allowed)
Solution¶
Show Solution
package main
import "fmt"
type AppError struct {
Code int
Message string
}
func (e *AppError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}
// BUGGY version — left here for comparison
func validateBuggy(n int) error {
var err *AppError // typed nil: *AppError, value == nil
if n < 0 {
err = &AppError{Code: 400, Message: "negative value not allowed"}
}
return err
// When n >= 0: returns interface{type=*AppError, value=nil}
// This interface is NOT nil — the type field is set.
// caller's "if err != nil" is ALWAYS true.
}
// CORRECT version — return untyped nil directly
func validate(n int) error {
if n < 0 {
return &AppError{Code: 400, Message: "negative value not allowed"}
}
return nil // interface{type=nil, value=nil} — truly nil
}
func main() {
// Demonstrate the bug:
err := validateBuggy(5)
fmt.Printf("buggy validate(5): err == nil? %v (BUG: should be true)\n", err == nil)
// Demonstrate the fix:
err = validate(5)
fmt.Printf("validate(5): err == nil? %v (correct)\n", err == nil)
err = validate(-1)
fmt.Printf("validate(-1): err == nil? %v (correct, error: %v)\n", err == nil, err)
}
Scoring Log¶
Record your performance honestly. Include the date and whether you used hints.
| Exercise | Date | Score | Used Hints? | Notes |
|---|---|---|---|---|
| Exercise 1 — fmt.Stringer | — | —/1 | — | — |
| Exercise 2 — Pointer Receiver & Interface | — | —/1 | — | — |
| Exercise 3 — io Interface Composition | — | —/2 | — | — |
| Exercise 4 — Type Switch | — | —/2 | — | — |
| Exercise 5 — sort.Interface | — | —/2 | — | — |
| Exercise 6 — Domain Interface Design | — | —/3 | — | — |
| Exercise 7 — Typed-Nil Bug Hunt | — | —/3 | — | — |
| Total | —/14 |
Passing threshold: 9/14 (65%). Aim for 12/14 (85%) before taking the test.