Embedding and Composition

Hey! If you love Go and building Go apps as much as I do, let's connect on Twitter or LinkedIn. I talk about this stuff all the time!

Want to learn how to build better Go applications faster and easier? You can.

Check out my course on the Go Standard Library. You can check it out now for free.


What is Embedding?

Think of embedding in Go like putting ingredients together to make a delicious dish. In this case, the “ingredients” are types (like structs) that you combine to create a more complex type. It’s a way to extend existing types with new functionality without resorting to inheritance.

In programming terms, embedding means placing the type of another variable inside a struct. This allows the embedded variable to inherit all the properties and methods of the original type, effectively adding functionality to it.

A Simple Example:

Imagine we have a structure representing a Car:

type Car struct {
  Brand string
  Model string
}

We can create a new Vehicle type that “inherits” from Car:

type Vehicle struct {
  Wheels int
  Engine string
  // ... other fields
}

Now, let’s embed Vehicle into CarWithTrailer:

type CarWithTrailer struct {
  Vehicle // Embed the Vehicle type
  Trailer bool
}

Understanding the Code:

Let’s break down what’s happening here:

  1. Defining the Types: We start by defining two simple structs: Car and Vehicle.
  2. Embedding: The CarWithTrailer struct embeds Vehicle, meaning it can access all of Vehicle’s fields and methods.

Benefits of Embedding:

  • Reusability: A single Vehicle type can be used by multiple structs, allowing for code sharing and efficiency.
  • Code Clarity: It allows us to clearly show the relationship between different types of objects.
  • Flexibility: You can add fields and methods to a base type without modifying the original structure.

Composition Example:

The example above shows how you can create a new “subtype” CarWithTrailer that has an “is-a” relationship with the Vehicle struct.

// Embedding allows us to reuse the properties and methods of the "Vehicle" struct 

type Vehicle struct {
  Engine string
  Wheels int
}

type CarWithTrailer struct {
  Vehicle // This is where we embed the "Vehicle" struct
  Towing bool
}

func (c *Vehicle) start() {
  fmt.Println("Starting the vehicle engine...")
}

func main() {
  c := CarWithTrailer{
    Vehicle: Vehicle{
      Engine: "V8",
      Wheels: 4,
    },
    Towing: true,
  }

  c.start() // Access the start method from Vehicle
}

In this case, CarWithTrailer has all the properties of Vehicle, including its methods.

Why is Composition Used?

  • Code Organization: Instead of adding fields and methods directly to a type like Car, the CarWithTrailer struct inherits them, making the code more structured and readable.
  • Flexibility: If Vehicle already has fields for “engine” and “wheels”, we can reuse the Vehicle type by simply embedding it in the CarWithTrailer struct.

Common Mistakes Beginners Make:

  • Confusing Embedding with Inheritance: Remember, Go doesn’t have traditional inheritance like some other languages.
  • Using Embedded Structs for “has-a” Relationships: A common mistake is to use embedded structs only for composition purposes. It’s important to remember that embedding the Vehicle struct into the CarWithTrailer type can be used to extend existing functionality, but it doesn’t directly define a hierarchy.
  • Not Using Interfaces: While you can use composition in Go, remember that interfaces are a powerful tool for achieving polymorphism and flexibility. Consider using an interface for functionalities like defining behavior instead of concrete implementation.

Best Practices for Using Structs with Embedding:

  • Use interfaces: In Go, embedding structs is often used as a way to define a “composite” type, inheriting properties from other structs.
  • Keep it simple: Don’t overthink the need to “inherit” everything.
  • Use composition for “has-a” relationships: Use a Vehicle struct with methods that describe the functionality of a vehicle, and then embed it into other structs.

Best Practices for Struct Embedding:

  • Keep the embedded type small: A large struct shouldn’t be embedded as it can lead to unnecessary memory allocation and complexity.
  • Use interfaces to define common behavior: This allows you to use polymorphism without relying on embedding.
  • Think about your data structure: Is there a clear “has-a” relationship with the embedded type? If so, consider using an interface instead of a struct.

Let’s revisit our initial example and see how it can be used in practice:

type Vehicle struct {
  Engine string
  Wheels int
}

type Car struct {
  Vehicle // This is the "is-a" relationship
}

func (v *Vehicle) startEngine() {
  fmt.Println("Starting the vehicle engine...")
}

func main() {
  c := Car{
    Vehicle: Vehicle{
      Engine: "Electric",
      Wheels: 4,
    },
  }

  c.startEngine() // Calling startEngine method from Vehicle
}

Embedding for Specific Use Cases:

  • Composition over inheritance: Always prefer composition for code reuse and flexibility.

Example:

type Vehicle struct {
  EngineType string 
  Wheels int
}

type Car struct {
  Vehicle // This is the "is-a" relationship
}

func main() {
  v := &Vehicle{EngineType: "Internal Combustion", Wheels: 4}
  c := &Car{Vehicle: *v}

  fmt.Println("Car engine type:", c.EngineType) // Access embedded field
}

Conclusion

Embedding and composition in Go provide powerful ways to organize and reuse code. By understanding and properly using these features, you can write more modular, flexible, and maintainable Go programs. Remember to use interfaces for defining behaviors and to keep your embedded structs small and focused on specific functionalities. Happy coding!



Stay up to date on the latest in Coding for AI and Data Science

Intuit Mailchimp