Menu
Coddy logo textTech

Polymorphic Collections

Part of the Object Oriented Programming section of Coddy's GO journey — lesson 44 of 107.

One of the most powerful applications of interfaces is storing different types in the same collection. A slice of an interface type can hold any value that satisfies that interface, letting you group related but different types together.

Consider a scenario where you need to manage various shapes. Instead of separate slices for each type, you can use a single slice of the interface type:

type Shape interface {
    Area() float64
}

type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }

type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }

func main() {
    shapes := []Shape{
        Circle{Radius: 5},
        Rectangle{Width: 4, Height: 3},
        Circle{Radius: 2},
    }
    
    for _, s := range shapes {
        fmt.Printf("Area: %.2f\n", s.Area())
    }
}

The shapes slice holds both circles and rectangles. When iterating, each element responds to Area() according to its actual type. This is polymorphism applied to collections—the same loop handles all shape types uniformly.

Polymorphic collections are essential when building systems that process varied items: a notification system sending emails and SMS messages, a game updating different enemy types, or a document processor handling multiple file formats. The collection doesn't care about specific types—only that each element provides the required behavior.

challenge icon

Challenge

Easy

Let's build a task management system that handles different types of tasks through a unified interface. You'll create various task types and process them all together in a polymorphic collection.

You'll organize your code across three files:

  • task.go: Define a Task interface that requires a Summary() string method. This interface will be the common contract that all task types must fulfill.
  • types.go: Create three different task types that each implement the Task interface:
    • BugFix with ID (string) and Severity (string) fields—its Summary() returns Bug #[ID] ([Severity])
    • Feature with Name (string) and Points (int) fields—its Summary() returns Feature: [Name] - [Points] pts
    • Documentation with Topic (string) field—its Summary() returns Docs: [Topic]
  • main.go: Create a function called PrintBacklog that accepts a slice of Task and prints each task's summary on its own line. Read task details from input, create one of each task type, collect them all into a single []Task slice, and pass it to PrintBacklog.

The following inputs will be provided:

  • Line 1: Bug ID
  • Line 2: Bug severity
  • Line 3: Feature name
  • Line 4: Feature points (integer)
  • Line 5: Documentation topic

For example, given 1042, critical, Dark Mode, 8, and API Reference, your output should be:

Bug #1042 (critical)
Feature: Dark Mode - 8 pts
Docs: API Reference

The power here is that PrintBacklog doesn't need separate logic for bugs, features, or documentation—it simply iterates through the slice and calls Summary() on each element. Each task type responds with its own unique format, demonstrating how polymorphic collections let you process diverse types uniformly.

Cheat sheet

A slice of an interface type can hold any value that satisfies that interface, enabling polymorphic collections:

type Shape interface {
    Area() float64
}

type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }

type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }

func main() {
    shapes := []Shape{
        Circle{Radius: 5},
        Rectangle{Width: 4, Height: 3},
        Circle{Radius: 2},
    }
    
    for _, s := range shapes {
        fmt.Printf("Area: %.2f\n", s.Area())
    }
}

The shapes slice holds different types (Circle and Rectangle) that all implement the Shape interface. When iterating, each element responds to Area() according to its actual type—this is polymorphism applied to collections.

This pattern is useful for processing varied items uniformly: notification systems handling different message types, games updating various enemy types, or document processors handling multiple file formats.

Try it yourself

package main

import (
	"fmt"
)

// TODO: Create a PrintBacklog function that accepts a slice of Task
// and prints each task's summary on its own line

func main() {
	// Read input
	var bugID string
	var bugSeverity string
	var featureName string
	var featurePoints int
	var docTopic string

	fmt.Scanln(&bugID)
	fmt.Scanln(&bugSeverity)
	fmt.Scanln(&featureName)
	fmt.Scanln(&featurePoints)
	fmt.Scanln(&docTopic)

	// TODO: Create one of each task type (BugFix, Feature, Documentation)

	// TODO: Collect all tasks into a single []Task slice

	// TODO: Call PrintBacklog with the slice of tasks
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming