In Go, struct and interface implementation are two powerful ways to organize code efficiently without the complicated problems that come with inheritance. This article delves deeper to model designs using struct, exploring various strategies, their benefits, and their trade offs.
A breakdown of the terms:
Struct composition: This is a technique where you create a new struct by embedding another struct inside of it. This allows you to reuse code and share functionality between different structs.
Interface implementation: This is a technique where you define a set of methods that a struct must implement. This allows you to separate your code and make it more flexible and reusable.
Inheritance: This is a technique where you create a new class by inheriting from another class. This allows you to reuse code and extend the functionality of existing classes.
- Basic Struct Composition: Struct composition is a method to reuse code and organize related entities. It embeds one struct inside another, encapsulating properties and behaviors.
type Base struct {
ID string
}
type User struct {
Base
Name string
}
Pros:
Code reuse: avoid repeating yourself by embedding existing structs into new structs. This makes the code more DRY (Don’t Repeat Yourself).
Organized code: Grouping related properties together in structs makes the code more readable and easier to maintain.
Cons:
No field overriding: If the embedded struct has fields with the same name as the outer struct, you can’t override the values of those fields. This can lead to ambiguity.
type User struct {
Name string
}
type Admin struct {
User
Name string // here field overrides the Name field from the embedded User struct.
}
func main() {
admin := Admin{
Name: "Anwar",
}
// What Name field is used here? That is ambiguous
fmt.Println(admin.Name)
}
This code will not compile because the compiler can’t determine which Name field to use. To avoid this ambiguity, you need to rename the field in the embedded struct or use anonymous fields.
- Enhanced Struct Composition:
Enhanced Struct Composition is a way to change or add behaviors to a struct without modifying the struct itself. You can do this by defining different methods or interfaces on the struct.
This is similar to how proxy models. A proxy model is a model that sits between your code and another model. It intercepts requests to the other model and can change or add behavior before the request is passed on.
type User struct {
Name string
}
func (u *User) PrintName() {
fmt.Println(u.Name)
}
type Admin struct {
User // Embeds the User struct
// Additional fields and methods for Admin users
}
func (a *Admin) CanPerformAdministrativeTasks() bool {
return true
}
func (a *Admin) CreateUser() {
// Create a new user struct
user := User{
Name: "New User",
}
// Save the new user to the database
// ...
// Print a message , or send an email
}
func main() {
// Create a regular user
user := User{
Name: "Anwar",
}
// Print the user's name
user.PrintName() // Prints "Anwar"
// Create an admin user
admin := Admin{
Name: "X",
}
// Print the admin user's name
admin.PrintName() // Prints "Admin: X"
// Check if the admin user can perform admin tasks
canPerformAdminTasks := admin.CanPerformAdministrativeTasks()
fmt.Println("Can perform administrative tasks:", canPerformAdminTasks) // Prints "true"
// Create a new user
admin.CreateUser()
}
Pros:
Behavior variability: Enhanced struct composition allows you to change or add behaviors to a struct without modifying the struct itself. This can be useful if you want to extend the functionality of a struct without breaking existing code.
Clean: Enhanced struct composition helps you to keep different behaviors separated and organized. This makes your code more readable and maintainable.
Cons:
Limited field modification: Enhanced struct composition only allows you to change or add behaviors to a struct. You cannot change the fields of the struct. This can be a limitation if you need to make changes to the fields of a struct.
- Structs with Interfaces: Using interfaces with struct offer flexible and clean design by specifying a set of methods that a type must implement.
type Printer interface {
PrintName()
}
type User struct {
Name string
}
func (u *User) PrintName() {
fmt.Println(u.Name)
}
Pros:
Enhanced Flexibility: Offers high flexibility by allowing different implementations of the interface.
Decoupling: Provides a clear separation between types and their implementations.
Cons: Overhead: this introduces some level of complexity and overhead, like for simple use-cases.
- Time-Stamped Structs: To advanced struct, consider using a TimeStamped struct. This holds the creation and modification timestamps, It is usful for various models that need to track time.
type TimeStamped struct {
CreatedAt time.Time
ModifiedAt time.Time
}
type User struct {
TimeStamped
Name string
}
This is helpful when several things need to keep track of when they were created and changed.