Interfaces & Error Handling
🟢 Level 1 – Foundations (Warm-up)
1️⃣ Define a Simple Interface
Create an interface called Printer with a method:
Print(message string) error
Then create a struct ConsolePrinter that implements it.
2️⃣ Implicit Implementation Check
Create another struct FilePrinter that also implements Printer.
Verify (at compile time) that both satisfy the interface using:
var _ Printer = (*YourStruct)(nil)
Explain what this line does.
3️⃣ Small Interface Principle
Define a large interface:
type UserService interface {
Create()
Update()
Delete()
Find()
}
Refactor it into smaller interfaces following Go best practices.
Explain why this is better.
4️⃣ Python Analogy Reflection
Write a short explanation:
- How is Go’s interface system similar to Python duck typing?
- What safety does Go provide that Python does not?
(No code required — conceptual.)
🟡 Level 2 – Error Handling Basics
5️⃣ Basic Error Return
Write a function:
Divide(a, b int) (int, error)
Return an error when dividing by zero.
6️⃣ Sentinel Error
Create a package-level error:
var ErrUserNotFound = errors.New("user not found")
Write a function FindUser(id string) that returns this error for unknown users.
7️⃣ Error Comparison
Write a small program that:
- Calls
FindUser - Uses
errors.Is()to check forErrUserNotFound - Prints a custom message
8️⃣ Error Wrapping
Modify FindUser so that it wraps the sentinel error using:
fmt.Errorf("database lookup failed: %w", ErrUserNotFound)
Then detect it properly using errors.Is().
🟠 Level 3 – Interfaces + Error Design
9️⃣ Storage Interface
Define an interface:
type Storage interface {
Save(data string) error
Get(id string) (string, error)
}
Create an InMemoryStorage implementation using a map.
🔟 Implement a Failing Storage
Create a second implementation:
type FailingStorage struct{}
Its Save() method should always return an error.
1️⃣1️⃣ Service Layer
Create a struct:
type UserService struct {
storage Storage
}
Add a method:
CreateUser(name string) error
Use storage.Save() internally.
1️⃣2️⃣ Error Propagation
Modify CreateUser to wrap storage errors using %w.
Test that the original error can still be detected using errors.Is().
🔵 Level 4 – Custom Errors
1️⃣3️⃣ Create a Custom Error Type
Create:
type ValidationError struct {
Field string
Message string
}
Implement the Error() method.
1️⃣4️⃣ Use Custom Error
Modify CreateUser so that:
- If
name == "", return aValidationError.
1️⃣5️⃣ Detect Custom Error
Write code that:
- Calls
CreateUser("") - Uses
errors.As()to extractValidationError - Prints the field name
🟣 Level 5 – Testing & Mocking
1️⃣6️⃣ Create a Mock Storage
Create:
type MockStorage struct {
ShouldFail bool
}
Return error when ShouldFail == true.
1️⃣7️⃣ Test Success Path
Write a unit test for CreateUser:
- Using
MockStoragewithShouldFail = false - Ensure no error is returned
1️⃣8️⃣ Test Failure Path
Write a test where:
MockStoragereturns error- Verify that
CreateUserwraps the error properly
1️⃣9️⃣ Test Validation Error
Write a test that:
- Calls
CreateUser("") - Asserts that returned error is
ValidationError - Use
errors.As()
🔴 Level 6 – Design Thinking Challenge
2️⃣0️⃣ Mini Architecture Exercise
Design a small notification system:
-
Define
Notifierinterface with:Send(message string) error -
Implement:
EmailNotifierSMSNotifier
- Create a
NotificationServicethat depends only onNotifier. - Properly wrap and propagate errors.
-
Write tests covering:
- Success
- Failure
- Error wrapping detection
This exercise should demonstrate:
- Interface-based design
- Error wrapping
- Mocking
- Test coverage of error paths
🎯 Expected Outcome After These Exercises
If student completes these:
- student understands implicit interfaces deeply
- student knows when and how to design interfaces
- student handles errors idiomatically
- student understands error wrapping
- student can use
errors.Is()anderrors.As() - student can mock dependencies cleanly
- student writes testable Go code