Let's think about this for a second
This lesson is tougher than the first Exercises lesson, combining the interface, custom error type, goroutine, and channel concepts you learned in the Advanced chapter into a real challenge. Each task reflects a pattern you'll commonly run into in real-world Go programs, giving you hands-on practice with polymorphism, safe error handling, and concurrent processing. Finishing this practice set will help you extend the Task Manager project from the Projects chapter on your own.
Exercises
Task 1: Define a Shape interface with an Area() float64 method. Create two structs, Circle{Radius float64} and Rectangle{Width, Height float64}, implement the Area() method for both, and run them together in a Shape slice. Task 2: Create a custom error type called InvalidAgeError (with an Error() string method), then write a validateAge(age int) error function that returns this custom error if age is less than 0 or greater than 150. Task 3: Using the worker pool pattern, write a program where a channel receives 10 numbers, 3 goroutines run in parallel, each squares its number, and sends the result back through a result channel.
Code Example
package main
import (
"fmt"
"sync"
)
// Task 1
type Shape interface {
Area() float64
}
type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
// Task 2
type InvalidAgeError struct{ Age int }
func (e *InvalidAgeError) Error() string {
return fmt.Sprintf("invalid age: %d", e.Age)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return &InvalidAgeError{Age: age}
}
return nil
}
// Task 3
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for n := range jobs {
results <- n * n
}
}
func main() {
shapes := []Shape{Circle{Radius: 2}, Rectangle{Width: 3, Height: 4}}
for _, s := range shapes {
fmt.Println("Area:", s.Area())
}
if err := validateAge(200); err != nil {
fmt.Println("Error:", err)
}
jobs := make(chan int, 10)
results := make(chan int, 10)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
for n := 1; n <= 10; n++ {
jobs <- n
}
close(jobs)
wg.Wait()
close(results)
for r := range results {
fmt.Println("squared:", r)
}
}It will print the Area() values for Circle and Rectangle, show a custom error message for age 200, and print 10 square values (in no particular order) for numbers 1-10 from the worker pool.Try it in 5 minutes
Within 5 minutes, change the worker count from 3 to 5 and observe how the output order changes, so you can see for yourself that goroutine scheduling isn't deterministic.
One quick word of caution
When running lots of goroutines, directly modifying a shared variable can cause a data race, so you should follow the Go idiom of communicating through channels instead - "don't communicate by sharing memory; share memory by communicating."