Skip to Content
Course content

15: Embedding Structs for Composition

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I’ve seen this happen with almost every developer I've mentored who comes from a Java or Python background: they see struct embedding in Go and immediately think, "Oh, great, inheritance!" They start treating the embedded struct as a base class and expect the outer struct to "be" an instance of the inner one. This is the fastest way to run into a wall of compiler errors in Go.

The "Is-A" Relationship Myth

In a class-based language, if a VideoFile inherits from MediaFile, a VideoFile is a MediaFile. You can pass it into any function that expects a MediaFile. In Go, embedding is not inheritance; it is composition. When you embed a type, you aren't creating a child class; you're just telling Go to "promote" the fields and methods of the inner struct to the outer one for convenience.

Let me show you exactly where this falls apart. Look at this code:

type MediaFile struct {
    Name string
    Size int
}

type VideoFile struct {
    MediaFile // Embedding
    Duration int
}

func PrintName(m MediaFile) {
    fmt.Println("File name is:", m.Name)
}

func main() {
    v := VideoFile{
        MediaFile: MediaFile{Name: "vacation.mp4", Size: 1024},
        Duration:   120,
    }

    // This will fail to compile!
    PrintName(v) 
}

The compiler will scream at you: cannot use v (type VideoFile) as type MediaFile in argument to PrintName. Even though VideoFile embeds MediaFile, it is not a MediaFile. It just happens to contain one.

Promoted Fields: Composition by Proxy

If it's not inheritance, why do we do it? Because it saves us from writing tedious "proxy" code. Without embedding, if you wanted to access the name of the media file through the video file, you'd have to write v.MediaFile.Name. That gets exhausting when you have multiple levels of nesting.

When you embed MediaFile into VideoFile, Go "promotes" those fields. You can now access v.Name directly. It feels like inheritance, but under the hood, Go is just doing the v.MediaFile.Name lookup for you. I personally find this a much cleaner way to build complex objects without the rigid, fragile hierarchies that plague traditional OOP.

Here is how you actually handle the "is-a" problem. You use an interface. If PrintName accepted an interface that defined a GetName() method, then both MediaFile and VideoFile could satisfy it.

type Namer interface {
    GetName() string
}

func (m MediaFile) GetName() string {
    return m.Name
}

func PrintName(n Namer) {
    fmt.Println("File name is:", n.GetName())
}

// Now PrintName(v) works because VideoFile "inherits" 
// the GetName method from the embedded MediaFile.

What Happens When Methods Clash

One thing that often trips people up is "shadowing." Since Go promotes methods from the embedded struct, what happens if both the outer and inner structs have a method with the same name? Go follows a simple rule: the outer struct always wins.

Imagine your MediaFile has a method called Describe(). If you also define Describe() on VideoFile, calling v.Describe() will execute the version on VideoFile. The embedded method isn't gone; it's just hidden. You can still reach it explicitly by calling v.MediaFile.Describe().

I usually recommend using this to your advantage. Use the embedded struct for the "default" behavior, and override specific methods in the outer struct only when the specialized type needs to do something different. It's a powerful way to share logic without locking yourself into a strict class tree.




📋 Practical Task

Implementing a Game Character Stat System

You are building a simple RPG. Instead of creating separate structs for every character type, you want to use composition to handle shared statistics. Your goal is to implement a system where different character types share a base set of stats but can override specific behaviors.

Requirements:

  • Create a BaseStats struct with fields for Health and Strength, and a method GetPowerLevel() int that returns the sum of health and strength.
  • Create a Warrior struct that embeds BaseStats and adds a Stamina field.
  • Create a Mage struct that embeds BaseStats and adds a Mana field.
  • The Mage needs to override the GetPowerLevel() method. For mages, the power level should be (Health + Strength) + (Mana / 2).
  • In your main function, instantiate both a Warrior and a Mage, and print their power levels to prove that the warrior uses the base logic while the mage uses the overridden logic.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.