composicion-vs-herencia-embedding-structs

Composition in Go with embedding and struct reuse

  • 4 min

Composition in Go is the way to reuse behavior by embedding one type inside another, without creating rigid class hierarchies.

If you come from Java, C# or C++, your brain is probably wired to think in terms of Inheritance. You look for the extends keyword. You want to create a base class Vehicle and have Car inherit from it to reuse the Start() method.

In Go, inheritance does not exist. There is no extends.

Instead of creating rigid “is a” hierarchies, Go lets you build types by assembling smaller pieces (“has a”). To make this composition convenient, it offers embedding.

Embedding: Anonymous fields

We briefly saw in the Structs article that we could put one struct inside another. But Embedding goes a step further.

When you declare a field inside a struct without giving it an explicit name (only specifying the type), Go performs Field and Method Promotion.

Practical example

Imagine a user system. We have a BaseUser with common data, and an Admin that needs that data plus some extra privileges.

package main

import "fmt"

// Base struct with common functionality
type BaseUser struct {
    ID     int
    Name string
}

// Method of the base struct
func (u BaseUser) Greet() {
    fmt.Printf("Hello, I'm %s (ID: %d)\n", u.Name, u.ID)
}

// 'Child' struct that EMBEDS the parent
type Admin struct {
    BaseUser // <--- Embedding: We don't give the field a name
    Level    int
}

func main() {
    admin := Admin{
        BaseUser: BaseUser{ID: 1, Name: "Root"},
        Level:    5,
    }

    // Direct access to fields (promotion)
    // No need to write admin.BaseUser.Name
    fmt.Println(admin.Name)

    // Direct access to methods (promotion)
    // Admin "inherits" the behavior of BaseUser
    admin.Greet() // Prints: Hello, I'm Root (ID: 1)
}
Copied!

Admin is not a BaseUser, but it contains one and can select its promoted fields and methods. It is a syntactic shorthand; the BaseUser field is still present.

Name Shadowing

What happens if the outer struct (Admin) defines a field or method with the same name as the inner struct (BaseUser)?

In classical inheritance, we would talk about Override. In Go, we talk about Shadowing.

The field from the outermost level takes precedence.

type Admin struct {
    BaseUser
    Name string // Conflict: BaseUser also has 'Name'
}

func main() {
    a := Admin{
        BaseUser: BaseUser{Name: "BaseName"},
        Name:      "AdminName",
    }

    fmt.Println(a.Name)            // Prints "AdminName" (the outer one wins)
    fmt.Println(a.BaseUser.Name)   // Prints "BaseName" (explicit access)
}
Copied!

This gives us full control. We are not overriding anything by surprise; the external field simply “shadows” the internal one, but the internal one is still there if we need it.

“Is a” vs. “Has a”

This is the point where classical OOP programmers often stumble.

In Java, if Car extends Vehicle, you can do: Vehicle v = new Car(); (Class polymorphism).

In Go, THIS DOES NOT WORK with Embedding.

func ProcessUser(u BaseUser) {
    // ...
}

func main() {
    admin := Admin{}

    // ERROR: cannot use admin (type Admin) as type BaseUser in argument to ProcessUser
    // ProcessUser(admin)
}
Copied!

Even though Admin has everything that BaseUser has, for the Go compiler they are completely different types. Admin is not a BaseUser.

How to achieve polymorphism

If we cannot pass an Admin where a BaseUser is expected, how do we write generic functions that accept both?

The answer is: Interfaces.

In Go, polymorphism is not achieved by type hierarchy (who your parent is), but by behavior (what methods you have). If Admin and BaseUser both have the Greet() method, we define a Greeter interface, and both will satisfy it.

Interface Composition

Embedding is not exclusive to structs. Interfaces can also be composed, and this is very common in the Go standard library.

For example, io.ReadWriter is simply the union of io.Reader and io.Writer.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface composition
type ReadWriter interface {
    Reader  // We embed the interface
    Writer  // We embed the interface
}
Copied!