Start learning free

LESSON 564 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Shapes Perimeter in Go: Interfaces and Polymorphism

Practice O(n) Read the full explanation

A drawing app keeps rectangles, squares and triangles in one list. Write the shape types so that code holding that list can ask any shape for its name and its perimeter (the length all the way around it) without knowing which kind of shape it is. That is polymorphism: one call, shape.perimeter(), and each kind of shape runs its own version.

Go has no classes and no inheritance, so the common type is an interface: any struct that has its methods is a Shape, with no implements keyword.

  • The interface Shape lists two methods: name() string returns the kind of shape as text, and perimeter() int returns the length around the shape.
  • Rectangle keeps a width and a height; NewRectangle(width, height) builds one. Its name is "Rectangle", its perimeter 2 × (width + height).
  • Square keeps a side; NewSquare(side) builds one. Its name is "Square", its perimeter 4 × side.
  • Triangle keeps three sides a, b and c; NewTriangle(a, b, c) builds one. Its name is "Triangle", its perimeter a + b + c.

The methods take the struct as a receiver named this, as in the course's other lessons. Then write two functions that get a slice of any shapes, []Shape:

  • totalPerimeter(shapes) returns the sum of the perimeters of all the shapes.
  • largestShape(shapes) returns the name of the shape with the largest perimeter. If several shapes share the largest perimeter, the one that comes first wins.

Both functions must work through the interface's methods only: never check which kind a shape is. Then a new kind of shape, added later, works with them unchanged.

The starter code already has every struct, constructor function and method header: declare the interface's methods and the structs' fields, and write the bodies. For each test, the harness reads the shapes, builds them in the order given, calls totalPerimeter and largestShape, and prints the two results on one line, separated by a space.

 

Example 1:

Input:
    NewRectangle(3, 4)
    NewSquare(5)
    NewTriangle(3, 4, 5)
Output: 46 Square
Explanation: The perimeters are 2 × (3 + 4) = 14, 4 × 5 = 20 and 3 + 4 + 5 = 12. Their total is 46, and the square's 20 is the largest.

Example 2:

Input:
    NewRectangle(2, 4)
    NewSquare(3)
    NewTriangle(4, 4, 4)
Output: 36 Rectangle
Explanation: All three perimeters are 12: 2 × (2 + 4), 4 × 3 and 4 + 4 + 4. On a tie the shape that comes first in the list wins, so the answer is the rectangle.

Example 3:

Input:
    NewSquare(1)
    NewRectangle(1, 1)
    NewTriangle(2, 2, 3)
Output: 15 Triangle
Explanation: The perimeters are 4, 4 and 7, so the last shape, the triangle, is the largest. A 1 by 1 rectangle has the same sides as a square, but it is still a Rectangle: each object answers name() for its own class.

 

Constraints:

  • Every test has between 1 and 100 shapes: the list is never empty.
  • Every size (width, height, side, and each side of a triangle) is an integer from 1 to 104.
  • A triangle's three sides always make a real triangle: each side is shorter than the other two together.

Note:

Each function looks at every shape once: O(n) time for n shapes, and no extra space.

STEP 1 OF 23
    Ctrl + Enter to check

    GO · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Shapes Perimeter in Go, explained

    Problem overview

    A drawing app keeps rectangles, squares and triangles in one list. The code that works with the list wants to ask every shape for its name and its perimeter, the length all the way around it, without knowing what kind of shape it holds. Rectangle, square and triangle each compute the perimeter differently, but the caller writes shape.perimeter() every time. That is polymorphism.

    You write the three shape types, the interface they share, and two functions over a []Shape: totalPerimeter adds up all the perimeters, and largestShape returns the name of the shape with the largest one, the first of them on a tie. For a 3 by 4 rectangle, a square of side 5 and a 3-4-5 triangle, the perimeters are 14, 20 and 12, so the answers are 46 and Square.

    Approach

    Go has no classes and no inheritance. To treat different structs alike, it uses an interface: a list of method signatures. Any type that has those methods is automatically a Shape. There is no implements keyword; the compiler checks the methods when you use a value as a Shape.

    With that in place, both functions are plain loops over the slice that call only name() and perimeter(). They never ask which struct is inside each element. A new kind of shape, written later, works with them unchanged.

    The alternative would be one function that checks each shape's kind and applies the matching formula. That puts every shape's rules in one place, and each new shape means editing that function again. With an interface, each shape keeps its own formula next to its own data, and the two functions stay short.

    Step by step

    1. Declare the interface Shape with two method signatures and no bodies: name() string and perimeter() int.
    2. Give Rectangle the fields width and height. Go has no constructors, so NewRectangle is an ordinary function that returns the struct, with each field named. Its name returns "Rectangle" and its perimeter returns 2 * (this.width + this.height).
    3. Square needs one field, side. Its perimeter is 4 * this.side.
    4. Triangle keeps three sides, a, b and c, and its perimeter is their sum.
    5. In totalPerimeter, start total at 0 and add shape.perimeter() for each element.
    6. In largestShape, start with largest := shapes[0], then replace it whenever a shape has a strictly greater perimeter. Strictly greater is what makes the first shape win a tie. Comparing the first shape with itself changes nothing, so the loop can start at index 0. Return largest.name() at the end.

    The solution in Go

    type Shape interface {
        name() string
        perimeter() int
    }
    
    type Rectangle struct {
        width int
        height int
    }
    
    func NewRectangle(width int, height int) Rectangle {
        return Rectangle{width: width, height: height}
    }
    
    func (this Rectangle) name() string {
        return "Rectangle"
    }
    
    func (this Rectangle) perimeter() int {
        return 2 * (this.width + this.height)
    }
    
    type Square struct {
        side int
    }
    
    func NewSquare(side int) Square {
        return Square{side: side}
    }
    
    func (this Square) name() string {
        return "Square"
    }
    
    func (this Square) perimeter() int {
        return 4 * this.side
    }
    
    type Triangle struct {
        a int
        b int
        c int
    }
    
    func NewTriangle(a int, b int, c int) Triangle {
        return Triangle{a: a, b: b, c: c}
    }
    
    func (this Triangle) name() string {
        return "Triangle"
    }
    
    func (this Triangle) perimeter() int {
        return this.a + this.b + this.c
    }
    
    func totalPerimeter(shapes []Shape) int {
        total := 0
        for _, shape := range shapes {
            total += shape.perimeter()
        }
        return total
    }
    
    func largestShape(shapes []Shape) string {
        largest := shapes[0]
        for _, shape := range shapes {
            if shape.perimeter() > largest.perimeter() {
                largest = shape
            }
        }
        return largest.name()
    }

    A few details:

    • The three structs never mention Shape. Each one becomes a Shape by having both methods with the right signatures.
    • largest := shapes[0] gives largest the type Shape, so later it can hold a rectangle, a square or a triangle.
    • The methods use value receivers, so a Rectangle value itself (not a pointer) has them and fits in a []Shape.
    • Struct fields start with a lowercase letter, so they stay inside the package. Outside code can only reach them through the methods.

    Using the shapes

    shapes := []Shape{NewRectangle(3, 4), NewSquare(5), NewTriangle(3, 4, 5)}
    fmt.Println(totalPerimeter(shapes), largestShape(shapes))
    for _, shape := range shapes {
        fmt.Println(shape.name(), shape.perimeter())
    }
    46 Square
    Rectangle 14
    Square 20
    Triangle 12
    

    A new shape needs nothing else. With this type added to the program:

    type Hexagon struct{ side int }
    
    func (this Hexagon) name() string   { return "Hexagon" }
    func (this Hexagon) perimeter() int { return 6 * this.side }

    then append(shapes, Hexagon{side: 4}) makes totalPerimeter return 70 and largestShape return Hexagon, with both functions untouched.

    Complexity

    Each function looks at every shape once, so both take O(n) time for n shapes. They use no extra space beyond a few variables: largest is just a copy of one interface value.

    Edge cases and common mistakes

    • Ties. With a rectangle 2 by 4, a square of side 3 and a triangle 4, 4, 4, every perimeter is 12. Using >= instead of > would pick the last shape; the lesson wants the first, which here is Rectangle. The total is 36.
    • One shape. The list is never empty, so shapes[0] exists. On an empty slice it would panic.
    • Same sides, different kind. A 1 by 1 rectangle has the same perimeter as a square of side 1, but its name() still says Rectangle, because each struct answers for itself.
    • Checking the type. Writing a type switch inside totalPerimeter would work today and break the moment a new shape appears. Call the interface methods only.
    • A missing method. If Square forgets perimeter, the compiler refuses to use a Square as a Shape and reports that it does not implement the interface and is missing the method perimeter.

    Testing

    Try the three examples above: a mixed list (46, Square), the tie of 12s (36, Rectangle), and NewSquare(1), NewRectangle(1, 1), NewTriangle(2, 2, 3), which gives 15 and Triangle because the last shape is the only one with perimeter 7.

    Keep going with Go500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free