Welcome back! In this final lesson of our Go Structs Basics Revision course, we will explore how Go promotes code reuse and flexibility through composition and methods. While some languages rely heavily on inheritance, Go prefers composition to share functionality between types, enhancing code readability and maintainability.
In this lesson, we'll explore how Go achieves shared behavior with composition by embedding structs and using interfaces. Our lesson plan includes understanding composition, leveraging embedded structs for shared attributes, utilizing methods to capture behavior, and learning Go-specific idioms for initializing structs. Ready? Let's dive in!
In Go, composition is favored over classical inheritance. Instead of inheriting from a base type, Go types can include or "embed" other types to reuse their functionality.
Here's an example using a struct named Vehicle
and another struct named Car
:
Go1package main 2 3import "fmt" 4 5// Define the base struct 'Vehicle' 6type Vehicle struct { 7 Color string 8 Brand string 9} 10 11// Define the struct 'Car' that embeds 'Vehicle' 12type Car struct { 13 Vehicle // Embedded Vehicle struct 14 Doors int 15} 16 17func main() { 18 // Create an instance of 'Car' with an embedded 'Vehicle' 19 myCar := Car{ 20 Vehicle: Vehicle{Color: "Red", Brand: "Toyota"}, 21 Doors: 4, 22 } 23 24 // Access embedded fields directly from 'Car' 25 fmt.Println("Car Brand:", myCar.Brand) 26 fmt.Println("Car Color:", myCar.Color) 27 fmt.Println("Number of Doors:", myCar.Doors) 28}
In this example, Car
includes Vehicle
as an embedded field. This allows Car
to directly access Vehicle
's fields (Color
and Brand
) without having to qualify them with the embedded struct's name (i.e., myCar.Vehicle.Color
). This demonstrates one way Go facilitates attribute reuse using composition.
In Go, we achieve shared attributes through embedded structs, which allow one struct to include fields from another.
Consider this example using a struct named Artist
and another struct named Musician
:
Go1package main 2 3import "fmt" 4 5// Define the base struct 'Artist' 6type Artist struct { 7 Name string 8} 9 10// Define the struct 'Musician' that embeds 'Artist' 11type Musician struct { 12 Artist // Embedded Artist struct 13 Instrument string 14} 15 16// Method to display the musician's details 17func (m Musician) Display() { 18 fmt.Printf("Name: %s\nInstrument: %s\n", m.Name, m.Instrument) 19} 20 21func main() { 22 // Create an instance of 'Musician' with an embedded 'Artist' 23 john := Musician{ 24 Artist: Artist{Name: "John Lennon"}, 25 Instrument: "Guitar", 26 } 27 john.Display() // Call the method to display details 28}
The Musician
struct includes Artist
as an embedded struct, allowing Musician
to access fields originating from Artist
(in this case, Name
). The Display
method illustrates how this struct pattern can express collective information using attributes from both the embedded and top-level structs.
In Go, methods are associated with receiver types, and embedded structs allow types to expose methods from other structs.
Here's an example where Vehicle
is a struct with a Start
method, and Car
embeds Vehicle
to reuse this method:
Go1package main 2 3import "fmt" 4 5// Define the base struct 'Vehicle' 6type Vehicle struct { 7 Brand string 8} 9 10// Method to simulate starting the vehicle 11func (v Vehicle) Start() { 12 fmt.Printf("The %s is starting.\n", v.Brand) 13} 14 15// Define the struct 'Car' that embeds 'Vehicle' 16type Car struct { 17 Vehicle // Embedded Vehicle struct 18} 19 20func main() { 21 // Create an instance of 'Car' and call the inherited 'Start' method 22 myCar := Car{ 23 Vehicle: Vehicle{Brand: "BMW"}, 24 } 25 myCar.Start() // Directly access the method from the embedded 'Vehicle' 26}
By embedding Vehicle
, Car
can use Vehicle
's Start
method without having to redefine it. This showcases how Go's composition model effectively allows for method reuse, providing inherited behavior more naturally compared to traditional inheritance.
Instead of constructors, Go promotes using struct literals and factory functions to initialize structs. This provides flexibility and readability:
Go1package main 2 3import "fmt" 4 5// Define a struct 'Product' 6type Product struct { 7 Name string 8 Price float64 9} 10 11// Factory function to create a new Product 12func NewProduct(name string, price float64) Product { 13 return Product{ 14 Name: name, 15 Price: price, 16 } 17} 18 19func main() { 20 // Initialize using a factory function 21 myProduct := NewProduct("Laptop", 999.99) 22 fmt.Printf("Product: %s, Price: $%.2f\n", myProduct.Name, myProduct.Price) 23}
Go encourages initializing data directly or through helper functions, like NewProduct
, for cleaner and more maintainable code. These factory functions can encapsulate any complex initialization logic, making the code using Product
easy to read and understand.
We've explored how Go handles attribute and method sharing using composition and interfaces. By embedding structs and utilizing methods effectively, Go ensures efficient code reuse without the complexity of inheritance. Practicing these concepts in Go will enhance your understanding and ability to write clean, maintainable code. Ready to try some exercises? Remember, programming is about exploring and problem-solving. Enjoy the journey!