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
- Declare the interface
Shapewith two method signatures and no bodies:name() stringandperimeter() int. - Give
Rectanglethe fieldswidthandheight. Go has no constructors, soNewRectangleis an ordinary function that returns the struct, with each field named. Itsnamereturns"Rectangle"and itsperimeterreturns2 * (this.width + this.height). Squareneeds one field,side. Its perimeter is4 * this.side.Trianglekeeps three sides,a,bandc, and its perimeter is their sum.- In
totalPerimeter, starttotalat 0 and addshape.perimeter()for each element. - In
largestShape, start withlargest := 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. Returnlargest.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 aShapeby having both methods with the right signatures. largest := shapes[0]giveslargestthe typeShape, so later it can hold a rectangle, a square or a triangle.- The methods use value receivers, so a
Rectanglevalue 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 isRectangle. 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 saysRectangle, because each struct answers for itself. - Checking the type. Writing a type switch inside
totalPerimeterwould work today and break the moment a new shape appears. Call the interface methods only. - A missing method. If
Squareforgetsperimeter, the compiler refuses to use aSquareas aShapeand reports that it does not implement the interface and is missing the methodperimeter.
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.
