Exercise: Interface Primer
Difficulty - Beginner
Learning Objectives
- Master Go interfaces and polymorphism
- Practice type assertions and type switches
- Understand interface composition
- Learn empty interface
- Implement common interfaces
Problem Statement
Create a shape library that demonstrates interface usage in Go. You'll define interfaces, implement them with different types, and use polymorphism.
Implement these components:
- Shape Interface: Common interface for all shapes
- Concrete Types: Circle, Rectangle, Triangle implementing Shape
- Drawable Interface: Extended interface with Draw method
- Type Assertions: Convert interface to concrete type
- Empty Interface: Work with
anytype
Interfaces and Types
1package shapes
2
3import "fmt"
4
5// Shape interface defines common shape operations
6type Shape interface {
7 Area() float64
8 Perimeter() float64
9}
10
11// Drawable extends Shape with visual representation
12type Drawable interface {
13 Shape
14 Draw() string
15}
16
17// Comparable allows shapes to be compared
18type Comparable interface {
19 IsLargerThan(other Shape) bool
20}
21
22// Circle implements Shape
23type Circle struct {
24 Radius float64
25}
26
27// Rectangle implements Shape
28type Rectangle struct {
29 Width float64
30 Height float64
31}
32
33// Triangle implements Shape
34type Triangle struct {
35 A, B, C float64 // sides
36}
Function Signatures
1// TotalArea calculates total area of multiple shapes
2func TotalArea(shapes ...Shape) float64
3
4// PrintShapes prints information about shapes
5func PrintShapes(shapes []Shape)
6
7// GetLargest returns the shape with largest area
8func GetLargest(shapes []Shape) Shape
9
10// GetCircles filters only circles from shapes
11func GetCircles(shapes []Shape) []*Circle
12
13// Describe returns detailed description of any value
14func Describe(value any) string
Example Usage
1package main
2
3import (
4 "fmt"
5 "shapes"
6)
7
8func main() {
9 // Create shapes
10 circle := &shapes.Circle{Radius: 5}
11 rectangle := &shapes.Rectangle{Width: 4, Height: 6}
12 triangle := &shapes.Triangle{A: 3, B: 4, C: 5}
13
14 // Use polymorphism
15 allShapes := []shapes.Shape{circle, rectangle, triangle}
16
17 // TotalArea
18 total := shapes.TotalArea(allShapes...)
19 fmt.Printf("Total area: %.2f\n", total)
20
21 // PrintShapes
22 shapes.PrintShapes(allShapes)
23
24 // GetLargest
25 largest := shapes.GetLargest(allShapes)
26 fmt.Printf("Largest shape has area: %.2f\n", largest.Area())
27
28 // Type assertion to get circles only
29 circles := shapes.GetCircles(allShapes)
30 fmt.Printf("Found %d circles\n", len(circles))
31
32 // Empty interface example
33 values := []any{42, "hello", circle, 3.14, true}
34 for _, v := range values {
35 fmt.Println(shapes.Describe(v))
36 }
37}
Requirements
- All concrete types must implement Shape interface
- Implement Stringer interface` method) for all shapes
- TotalArea should work with any number of shapes
- GetCircles should use type assertions safely
- Describe should handle different types gracefully
Solution
Click to see the complete solution
1package shapes
2
3import (
4 "fmt"
5 "math"
6)
7
8// Shape interface defines common shape operations
9type Shape interface {
10 Area() float64
11 Perimeter() float64
12}
13
14// Drawable extends Shape with visual representation
15type Drawable interface {
16 Shape
17 Draw() string
18}
19
20// Comparable allows shapes to be compared
21type Comparable interface {
22 IsLargerThan(other Shape) bool
23}
24
25// Circle implements Shape
26type Circle struct {
27 Radius float64
28}
29
30func Area() float64 {
31 return math.Pi * c.Radius * c.Radius
32}
33
34func Perimeter() float64 {
35 return 2 * math.Pi * c.Radius
36}
37
38func String() string {
39 return fmt.Sprintf("Circle(radius=%.2f)", c.Radius)
40}
41
42func Draw() string {
43 return " ○ "
44}
45
46func IsLargerThan(other Shape) bool {
47 return c.Area() > other.Area()
48}
49
50// Rectangle implements Shape
51type Rectangle struct {
52 Width float64
53 Height float64
54}
55
56func Area() float64 {
57 return r.Width * r.Height
58}
59
60func Perimeter() float64 {
61 return 2 *
62}
63
64func String() string {
65 return fmt.Sprintf("Rectangle(width=%.2f, height=%.2f)", r.Width, r.Height)
66}
67
68func Draw() string {
69 return " ▭ "
70}
71
72func IsLargerThan(other Shape) bool {
73 return r.Area() > other.Area()
74}
75
76// Triangle implements Shape
77type Triangle struct {
78 A, B, C float64 // sides
79}
80
81func Area() float64 {
82 // Heron's formula
83 s := / 2
84 return math.Sqrt(s * * *)
85}
86
87func Perimeter() float64 {
88 return t.A + t.B + t.C
89}
90
91func String() string {
92 return fmt.Sprintf("Triangle(sides=%.2f, %.2f, %.2f)", t.A, t.B, t.C)
93}
94
95func Draw() string {
96 return " △ "
97}
98
99func IsLargerThan(other Shape) bool {
100 return t.Area() > other.Area()
101}
102
103// TotalArea calculates total area of multiple shapes
104func TotalArea(shapes ...Shape) float64 {
105 total := 0.0
106 for _, shape := range shapes {
107 total += shape.Area()
108 }
109 return total
110}
111
112// PrintShapes prints information about shapes
113func PrintShapes(shapes []Shape) {
114 for i, shape := range shapes {
115 fmt.Printf("%d. %s - Area: %.2f, Perimeter: %.2f\n",
116 i+1, shape, shape.Area(), shape.Perimeter())
117 }
118}
119
120// GetLargest returns the shape with largest area
121func GetLargest(shapes []Shape) Shape {
122 if len(shapes) == 0 {
123 return nil
124 }
125
126 largest := shapes[0]
127 for _, shape := range shapes[1:] {
128 if shape.Area() > largest.Area() {
129 largest = shape
130 }
131 }
132
133 return largest
134}
135
136// GetCircles filters only circles from shapes
137func GetCircles(shapes []Shape) []*Circle {
138 var circles []*Circle
139
140 for _, shape := range shapes {
141 // Type assertion with comma-ok idiom
142 if circle, ok := shape.(*Circle); ok {
143 circles = append(circles, circle)
144 }
145 }
146
147 return circles
148}
149
150// Describe returns detailed description of any value
151func Describe(value any) string {
152 // Type switch on empty interface
153 switch v := value.(type) {
154 case int:
155 return fmt.Sprintf("Integer: %d", v)
156 case string:
157 return fmt.Sprintf("String: %q", v)
158 case float64:
159 return fmt.Sprintf("Float: %.2f", v)
160 case bool:
161 return fmt.Sprintf("Boolean: %t", v)
162 case Shape:
163 return fmt.Sprintf("Shape: %s", v, v.Area())
164 case nil:
165 return "Nil value"
166 default:
167 return fmt.Sprintf("Unknown type: %T", v)
168 }
169}
170
171// IsDrawable checks if a shape implements Drawable interface
172func IsDrawable(shape Shape) bool {
173 _, ok := shape.(Drawable)
174 return ok
175}
176
177// DrawAllDrawable draws all shapes that implement Drawable
178func DrawAllDrawable(shapes []Shape) string {
179 var result string
180
181 for _, shape := range shapes {
182 if drawable, ok := shape.(Drawable); ok {
183 result += drawable.Draw()
184 }
185 }
186
187 return result
188}
Explanation
Shape Interface:
- Defines common contract for all shapes
Area()andPerimeter()methods- Implemented by Circle, Rectangle, Triangle
Concrete Types:
- Each type stores necessary data
- Implements all Shape methods
- Also implements
String()for pretty printing
Drawable Interface:
- Embeds Shape interface
- Adds
Draw()method - Types can implement multiple interfaces
TotalArea:
- Accepts variadic Shape parameters
- Demonstrates polymorphism - works with any Shape
- Uses interface methods without knowing concrete type
PrintShapes:
- Takes slice of Shape interface
- Uses
String()method if available - Calls Area() and Perimeter() polymorphically
GetCircles:
- Uses type assertion to filter specific type
- Comma-ok idiom prevents panic
- Returns slice of concrete Circle pointers
Describe:
- Works with empty interface
- Uses type switch to handle different types
- Demonstrates type checking at runtime
Interface Patterns
1. Small Interfaces:
1// Good: focused interface
2type Reader interface {
3 Read(p []byte)
4}
5
6// Avoid: kitchen sink interfaces
2. Interface Composition:
1type ReadWriter interface {
2 Reader
3 Writer
4}
5
6type ReadWriteCloser interface {
7 Reader
8 Writer
9 Closer
10}
3. Accept Interfaces, Return Structs:
1// Good
2func Process(r io.Reader) {
3 // Implementation
4}
5
6// Avoid returning interfaces
7func Bad() io.Reader { // Less flexible
8 return &MyReader{}
9}
4. Type Assertions:
1// Safe: comma-ok idiom
2if concrete, ok := iface.(ConcreteType); ok {
3 // Use concrete
4}
5
6// Unsafe: will panic if wrong type
7concrete := iface.(ConcreteType)
5. Type Switch:
1switch v := iface.(type) {
2case string:
3 fmt.Println("String:", v)
4case int:
5 fmt.Println("Int:", v)
6default:
7 fmt.Println("Unknown")
8}
Common Standard Interfaces
fmt.Stringer:
1type Stringer interface {
2 String() string
3}
4
5func String() string {
6 return fmt.Sprintf("Circle(r=%.2f)", c.Radius)
7}
error:
1type error interface {
2 Error() string
3}
4
5type MyError struct {
6 Message string
7}
8
9func Error() string {
10 return e.Message
11}
io.Reader:
1type Reader interface {
2 Read(p []byte)
3}
sort.Interface:
1type Interface interface {
2 Len() int
3 Less(i, j int) bool
4 Swap(i, j int)
5}
Test Cases
1package shapes
2
3import (
4 "testing"
5)
6
7func TestCircleArea(t *testing.T) {
8 circle := &Circle{Radius: 5}
9 expected := 78.54 // Approximately
10
11 area := circle.Area()
12 if area < expected-0.01 || area > expected+0.01 {
13 t.Errorf("Expected area ~%.2f, got %.2f", expected, area)
14 }
15}
16
17func TestTotalArea(t *testing.T) {
18 shapes := []Shape{
19 &Circle{Radius: 1},
20 &Rectangle{Width: 2, Height: 3},
21 }
22
23 total := TotalArea(shapes...)
24
25 // Circle: π * 1^2 ≈ 3.14
26 // Rectangle: 2 * 3 = 6
27 // Total ≈ 9.14
28 expected := 9.14
29
30 if total < expected-0.1 || total > expected+0.1 {
31 t.Errorf("Expected total ~%.2f, got %.2f", expected, total)
32 }
33}
34
35func TestGetCircles(t *testing.T) {
36 shapes := []Shape{
37 &Circle{Radius: 1},
38 &Rectangle{Width: 2, Height: 3},
39 &Circle{Radius: 2},
40 &Triangle{A: 3, B: 4, C: 5},
41 }
42
43 circles := GetCircles(shapes)
44
45 if len(circles) != 2 {
46 t.Errorf("Expected 2 circles, got %d", len(circles))
47 }
48}
49
50func TestDescribe(t *testing.T) {
51 tests := []struct {
52 value any
53 contains string
54 }{
55 {42, "Integer"},
56 {"hello", "String"},
57 {3.14, "Float"},
58 {true, "Boolean"},
59 {&Circle{Radius: 5}, "Shape"},
60 }
61
62 for _, tt := range tests {
63 result := Describe(tt.value)
64 if result == "" {
65 t.Errorf("Describe returned empty string for %v", tt.value)
66 }
67 }
68}
69
70func TestInterfaceComposition(t *testing.T) {
71 circle := &Circle{Radius: 5}
72
73 // Circle implements Shape
74 var shape Shape = circle
75 if shape.Area() == 0 {
76 t.Error("Circle should implement Shape")
77 }
78
79 // Circle implements Drawable
80 var drawable Drawable = circle
81 if drawable.Draw() == "" {
82 t.Error("Circle should implement Drawable")
83 }
84}
Bonus Challenges
- Generic Shape Factory: Create shapes from string names
1func CreateShape(shapeType string, params ...float64)
- Shape Collections: Implement sort.Interface for shapes
1type ByArea []Shape
2
3func Len() int
4func Less(i, j int) bool
5func Swap(i, j int)
- Visitor Pattern: Implement visitor for shapes
1type ShapeVisitor interface {
2 VisitCircle(*Circle)
3 VisitRectangle(*Rectangle)
4 VisitTriangle(*Triangle)
5}
- JSON Marshaling: Custom JSON representation
1func MarshalJSON()
2func UnmarshalJSON(data []byte) error
Key Takeaways
- Interfaces define behavior, not data
- Implicit implementation - no "implements" keyword needed
- Accept interfaces, return structs for flexibility
- Small interfaces are more composable
- Empty interface can hold any value
- Type assertions convert interface to concrete type
- Type switches handle multiple types
- Interface composition creates larger interfaces from smaller ones
Interfaces are Go's primary abstraction mechanism. Understanding interfaces deeply is crucial for writing idiomatic, flexible Go code. They enable polymorphism without inheritance and allow for powerful design patterns.