Start learning free

LESSON 191 OF 338 · OBJECT-ORIENTED PROGRAMMING

OOP Capstone in Go: Animal Hierarchy with Interfaces

Lesson Read the full explanation

TL ; DR:

Put it all together: an interface, three concrete structs, and a polymorphic loop that dispatches by runtime type. This is the shape of nearly every real OOP design.


type Animal interface {
    sound() string   // each implementor picks
}
type BaseAnimal struct{ name string }
func (b BaseAnimal) speak(s string) {  // shared by all
    fmt.Printf("%s: %s\n", b.name, s)
}
// Dog, Cat, Bird embed BaseAnimal and implement sound
// animals := []Animal{Dog{BaseAnimal{"Rex"}}, ...}
// for _, a := range animals { a.speak() }


Full lesson:

This problem stitches together everything you've learned in this chapter:

  1. Interfaces — three concrete structs share a common interface.
  2. Struct initialization — each struct sets a name field when created.
  3. Interface methods — the interface declares sound() with no body; each struct must provide one.
  4. Shared behavior — a helper prints the name and the result of sound(); every implementor benefits from it.
  5. Polymorphism — a slice typed as Animal holds a Dog, a Cat, and a Bird; speak() dispatches to the right sound() at runtime.

The shape of this code is the shape of nearly every real OOP design you'll see — a small contract at the top (the interface), concrete variants below, and the rest of the program written against the abstract type. New variants get added without touching the consumer code.


Your task

The Animal interface is already defined, with a concrete speak() that calls the abstract sound(). Add three structs — Dog (returns "Woof"), Cat (returns "Meow"), and Bird (returns "Tweet"). Then in mainFunction, build a polymorphic collection holding one of each (Rex, Whiskers, Tweety in that order) and loop through it calling speak().

Expected output:

Rex: Woof
Whiskers: Meow
Tweety: Tweet
STEP 1 OF 6
    Ctrl + Enter to check

    GO · OBJECT-ORIENTED PROGRAMMING

    Oop Capstone in Go, explained

    What the capstone puts together

    This exercise combines the object-oriented pieces of Go in one small program: an interface, three structs that satisfy it, and a loop that treats them all alike. Go has no classes, so the pieces look a little different from other languages:

    • Interface. Animal is a contract: a type that has a speak() method is an Animal.
    • Structs. Dog, Cat and Bird each hold a name field. A struct is plain data; behaviour is attached to it with methods.
    • Methods with receivers. func (this Dog) speak() attaches speak to Dog. The receiver, written before the method name, plays the role of self or this.
    • Implicit satisfaction. No type says "I implement Animal". Having the method is enough.
    • Polymorphism. A slice of type []Animal can hold a Dog, a Cat and a Bird, and calling speak() on each element runs the right version.

    The shape of the program is common in real Go code: a small interface at the top, concrete types below it, and the rest of the code written against the interface. Adding a fourth animal means writing one more struct and method; the loop stays as it is.

    The task

    The starter code declares Animal with a single method, speak(), and already contains mainFunction, which builds a slice of three animals named Rex, Whiskers and Tweety and calls speak() on each. What is missing are the three types. The expected output is:

    Rex: Woof
    Whiskers: Meow
    Tweety: Tweet

    Step by step

    1. Define Dog as a struct with a name string field. The slice literal in mainFunction writes Dog{name: "Rex"}, so the field must be called name. The struct holds data only.
    2. Give Dog a speak method. It prints the name and the sound with fmt.Printf("%s: Woof\n", this.name). Printf does not end the line by itself, so the format string needs the \n. With this method, Dog satisfies Animal.
    3. Repeat for Cat. Same field, same method name and signature, with Meow in the format string.
    4. Repeat for Bird, printing Tweet.
    5. Run the loop. Once all three types have speak(), the literal []Animal{...} compiles, and for _, a := range animals calls a.speak() on each element in order. Each call is dispatched to the method of the type stored in a.

    The solution in Go

    type Animal interface {
        speak()
    }
    
    type Dog struct {
        name string
    }
    
    func (this Dog) speak() {
        fmt.Printf("%s: Woof\n", this.name)
    }
    
    type Cat struct {
        name string
    }
    
    func (this Cat) speak() {
        fmt.Printf("%s: Meow\n", this.name)
    }
    
    type Bird struct {
        name string
    }
    
    func (this Bird) speak() {
        fmt.Printf("%s: Tweet\n", this.name)
    }
    
    
    
    func mainFunction() {
        animals := []Animal{Dog{name: "Rex"}, Cat{name: "Whiskers"}, Bird{name: "Tweety"}}
        for _, a := range animals {
            a.speak()
        }
    }

    Details worth knowing

    • The receiver name. The solution calls it this, which reads naturally if you come from other languages, but it is an ordinary identifier in Go. Go code usually uses a short name taken from the type, such as d for Dog.
    • Value receivers. The methods receive a copy of the struct. That is fine here because they only read name. A method that changed a field would need a pointer receiver, func (d *Dog) ..., and then only *Dog would satisfy the interface.
    • Keyed fields. Dog{name: "Rex"} names the field. Fields left out take their zero value, so Dog{} has name equal to "".
    • The repeated lines. Three nearly identical methods are normal in Go. The language prefers small, explicit pieces over inheritance.

    Sharing behaviour: embedding, not inheritance

    If every animal needs the same helper, Go offers struct embedding. A struct can contain another struct without giving the field a name, and the outer type gains the inner type's fields and methods:

    type Sounder interface{ sound() string }
    
    type Base struct{ name string }
    
    func (b Base) say(s Sounder) {
        fmt.Printf("%s: %s\n", b.name, s.sound())
    }
    
    type Cow struct{ Base }
    
    func (c Cow) sound() string { return "Moo" }
    
    c := Cow{Base{"Daisy"}}
    c.say(c)
    Daisy: Moo

    Note the argument in c.say(c). Embedding is not inheritance: inside Base.say, the receiver is a Base, which knows nothing about the Cow around it, so it cannot call sound on its own. The outer value has to be passed in explicitly. This is why the capstone gives each type its own complete speak method.

    Common mistakes

    • Forgetting the field name. The loop builds Dog{name: "Rex"}, so a struct with a field called Name or no field at all does not compile.
    • Forgetting \n. Without it, the three lines run together as Rex: WoofWhiskers: MeowTweety: Tweet.
    • Misspelling the method. A method called Speak or speek does not satisfy Animal, and the slice literal fails with does not implement Animal (missing method speak). Go is case sensitive.
    • Changing the signature. A speak() string method does not satisfy an interface asking for speak().
    • Looking for implements. There is none; the compiler checks the methods where the value is used as an Animal.

    Key takeaways

    • Structs hold the data, methods with receivers hold the behaviour, and interfaces describe the behaviour code depends on.
    • A type satisfies an interface by having its methods, with no declaration.
    • A slice of an interface type can mix concrete types, and each method call goes to the right one.
    • Embedding shares fields and methods, but it does not give the shared code access to the outer type's overrides.
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free