Interface Primer

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:

  1. Shape Interface: Common interface for all shapes
  2. Concrete Types: Circle, Rectangle, Triangle implementing Shape
  3. Drawable Interface: Extended interface with Draw method
  4. Type Assertions: Convert interface to concrete type
  5. Empty Interface: Work with any type

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

  1. All concrete types must implement Shape interface
  2. Implement Stringer interface` method) for all shapes
  3. TotalArea should work with any number of shapes
  4. GetCircles should use type assertions safely
  5. 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() and Perimeter() 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

  1. Generic Shape Factory: Create shapes from string names
1func CreateShape(shapeType string, params ...float64)
  1. 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)
  1. Visitor Pattern: Implement visitor for shapes
1type ShapeVisitor interface {
2    VisitCircle(*Circle)
3    VisitRectangle(*Rectangle)
4    VisitTriangle(*Triangle)
5}
  1. 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.