Start learning free

LESSON 188 OF 338 · OBJECT-ORIENTED PROGRAMMING

Polymorphism in Go: Interfaces and Dynamic Dispatch

Lesson Read the full explanation

TL ; DR:

Polymorphism is when a single piece of code (like animal.speak()) does different things depending on which subclass the object actually is. You write the call once, and the right override runs automatically.


animals := []Animal{Dog{}, Cat{}}
for _, a := range animals {
    a.speak()   // dispatches to Dog.speak then Cat.speak
}


Full lesson:

Polymorphism (Greek for "many forms") is what makes inheritance and interfaces actually useful at scale. Once Dog and Cat both inherit from Animal and override speak(), you can stop caring which one you're holding. Any code that takes an Animal and calls speak() works for both.

Why it matters

Imagine you're writing a function that prints what every animal in a zoo says:

func roll(animals []Animal) {
    for _, a := range animals {
        a.speak()
    }
}

This function works for any current or future kind of animal. Add a Cow subclass tomorrow — this loop runs Cow.speak() automatically, no edits needed. That's the win: code that works today and keeps working as the type hierarchy grows.

Static type vs dynamic type

The reference is typed as Animal. The object it actually points to is a Dog, or a Cat, etc. Polymorphism is the language dispatching the method call based on the dynamic type (what it is at runtime) rather than the static type (what the variable was declared as).

Notes by language

  • C# / C++ — the parent method must be virtual (and the override marked override) for runtime dispatch. Otherwise you get static dispatch and the wrong version runs.
  • Java / JS / Python — methods are virtual by default. Just override and it works.

Your task

Animal, Dog, and Cat are already defined, with the right overrides. Inside mainFunction, build a collection containing one Dog and one Cat (typed as Animal), then loop through it calling speak() on each.

Expected output:

Woof
Meow
STEP 1 OF 3
    Ctrl + Enter to check

    GO · OBJECT-ORIENTED PROGRAMMING

    Polymorphism in Go, explained

    One call, many behaviours

    Polymorphism (Greek for "many forms") means one piece of code does different things depending on the actual kind of value it is given. You write a.speak() once, and the right version runs for each value. In Go, polymorphism comes from interfaces.

    Interfaces are satisfied implicitly

    An interface is a list of method signatures:

    type Animal interface {
        speak()
    }
    

    Any type that has all the listed methods is an Animal. There is no implements keyword and no inheritance relationship to declare. Dog and Cat below are unrelated types, and each qualifies simply because it has a speak() method.

    type Dog struct {}
    
    func (this Dog) speak() {
        fmt.Println("Woof")
    }
    
    type Cat struct {}
    
    func (this Cat) speak() {
        fmt.Println("Meow")
    }
    

    Using the interface

    A variable of interface type can hold a value of any type that satisfies it. A slice of the interface type can mix them:

    animals := []Animal{Dog{}, Cat{}}
    for _, a := range animals {
        a.speak()
    }
    

    This loop prints Woof and then Meow. The loop variable a has the type Animal, and the method that runs is chosen by the concrete type stored inside it. That choice happens while the program runs, which is called dynamic dispatch.

    Static type and dynamic type

    The static type of a is what the code says: Animal. Its dynamic type is what it holds at run time: Dog for the first element, Cat for the second. Printing with fmt.Printf("%T\n", a) shows the dynamic type, main.Dog and main.Cat in this program.

    Why it matters

    A function that takes []Animal and calls speak on each element works with every type that exists now and any that you add later. If you add a Cow type with a speak method, the loop needs no edits:

    type Cow struct{}
    
    func (c *Cow) speak() { fmt.Println("Moo") }
    
    animals := []Animal{Dog{}, Cat{}, &Cow{}}
    

    Notice &Cow{}. Because speak has a pointer receiver here, only a *Cow satisfies Animal. Putting a plain Cow{} in the slice is a compile error: "method speak has pointer receiver". The rule is that the method set of a value type contains only value-receiver methods, while the pointer type has both.

    Getting the concrete type back

    Sometimes you need to know which type an interface value holds. A type assertion with the two-value form checks it safely:

    if d, ok := animals[0].(Dog); ok {
        fmt.Println("the first animal is a Dog", d)
    }
    

    If the stored value is not a Dog, ok is false and nothing breaks. Reach for this sparingly: if you are checking types all the time, the interface probably needs another method instead.

    The lesson's exercise

    The Animal interface and the two types are already written. You write the code that uses them.

    1. Declare mainFunction with no parameters and no result.
    2. Build the slice with animals := []Animal{Dog{}, Cat{}}. Each element is a concrete struct value wrapped in the interface.
    3. Range over it, discarding the index with _, and call a.speak() on each element.
    type Animal interface {
        speak()
    }
    
    type Dog struct {}
    
    func (this Dog) speak() {
        fmt.Println("Woof")
    }
    
    type Cat struct {}
    
    func (this Cat) speak() {
        fmt.Println("Meow")
    }
    
    
    
    func mainFunction() {
        animals := []Animal{Dog{}, Cat{}}
        for _, a := range animals {
            a.speak()
        }
    }
    

    The output is:

    Woof
    Meow
    

    Common mistakes

    • Expecting inheritance. Dog and Cat do not inherit from Animal. They satisfy it by having the method, which is checked by the compiler where a value is assigned to an Animal.
    • Missing a method. A type that lacks speak cannot go in the slice, and the compiler says which method is missing.
    • Mixing up pointer and value receivers, as described above.
    • Calling a method on a nil interface. A variable declared as var a Animal holds nothing, so a.speak() stops the program with a nil pointer panic.
    • Forgetting the underscore. for i, a := range animals with i unused does not compile. Write _ for the index you ignore.

    Key takeaways

    • An interface lists methods, and any type with those methods satisfies it automatically.
    • Calling a method on an interface value runs the method of the stored concrete type.
    • That lets one loop handle current and future types without edits.
    • Whether a value or a pointer satisfies an interface depends on the receiver types.
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free