10.4 Struct Embedding: Promoting Fields and Methods
Right, so you’ve got your structs defined. You’ve got your User with a Name and an ID. Neat. But now you’re probably thinking, “I’ve got this AdminUser that’s like a User but with, you know, admin powers.” Your first instinct might be to reach for inheritance. Stop it. This isn’t that kind of party. Go offers a different, and frankly more composable, approach: embedding.
Think of it as structural delegation, not inheritance. You’re embedding one struct inside another, and the fields and methods of the inner struct get promoted to the outer struct. It’s like the outer struct suddenly gets all the abilities of the inner one without you having to write a bunch of tedious pass-through methods.
The Basic Syntax: It’s Just a Type
The syntax is deceptively simple. You don’t give the field a name; you just provide the type. Let’s get concrete.
type User struct {
ID int
Name string
}
func (u User) DisplayName() string {
return fmt.Sprintf("%s (%d)", u.Name, u.ID)
}
// AdminUser embeds the User struct
type AdminUser struct {
User // The embedded type: no field name, just the type.
IsSuperAdmin bool
}
Now, watch the magic. Because User is embedded (not named), all of its fields and methods are available directly on an AdminUser instance.
func main() {
admin := AdminUser{
User: User{ // Note: the field name here is the TYPE name, 'User'
ID: 1,
Name: "Tricia",
},
IsSuperAdmin: true,
}
// These are promoted from the embedded User struct.
fmt.Println(admin.ID) // Direct access to User.ID
fmt.Println(admin.Name) // Direct access to User.Name
fmt.Println(admin.DisplayName()) // Direct access to User.DisplayName()
// And of course, access to its own field.
fmt.Println(admin.IsSuperAdmin)
}
This is the core of it. The AdminUser has-a User, but the language lets you interact with it as-if the AdminUser is-a User for those specific fields and methods. It’s a fantastic way to model composition and share behavior.
How Promotion Really Works (The Truth)
Here’s the part the manuals often gloss over: there is no real “promotion” happening at runtime. The compiler is just doing a bit of syntactic sugar for you. When you write admin.Name, the compiler checks if AdminUser has a Name field. It doesn’t. So then it checks if AdminUser has any embedded fields that do have a Name field. It finds one in User. So what it actually does is resolve it to admin.User.Name.
This has a huge implication: embedding is not a relationship between types; it’s a relationship between structs. The AdminUser type doesn’t satisfy an interface that requires a DisplayName() method because it embeds User; it satisfies it because it has a DisplayName() method, which it got by having a User field whose method it can call. This distinction is subtle but crucial for understanding why things work the way they do.
The Name Collision Pitfall (And How to Resolve It)
What happens if the outer struct and the embedded struct have a field or method with the same name? Let’s be honest, the designers picked the obvious path here: the outer struct wins. It’s like a variable shadowing in a narrower scope.
type Conflicting struct {
User
ID string // This ID field shadows the embedded User.ID (an int)
Name int // Godspeed to anyone who does this intentionally.
}
func main() {
c := Conflicting{
User: User{ID: 10, Name: "Alice"},
ID: "ten",
Name: 99, // This is an int now. Why? Who knows. Don't do this.
}
fmt.Println(c.ID) // "ten" (the string from Conflicting)
fmt.Println(c.Name) // 99 (the int from Conflicting)
// You can still get to the embedded ones by using the full path.
fmt.Println(c.User.ID) // 10
fmt.Println(c.User.Name) // "Alice"
}
This is your best practice: avoid name collisions like the plague. It creates confusing, brittle code. If you need to have a field with the same name, you should probably not be embedding that struct, or you should rename one of the fields. The ability to access the inner field via c.User.ID is a safety net, not a feature you should design around.
Embedding For Interfaces: The Ultimate Power Move
This is where embedding shifts from a neat trick to a paradigm-defining feature. You can embed interfaces within structs. This lets you create structs that must fulfill an interface by composing other structs, often at runtime.
Imagine a Client interface. Now you can create a LoggingClient that adds logging around any other client.
type Client interface {
Do(req *http.Request) (*http.Response, error)
}
// LoggingClient embeds the Client interface.
// It must be constructed with a concrete Client that fulfills it.
type LoggingClient struct {
Client // Embed the interface, not a concrete type.
}
// Now LoggingClient must implement Client itself. We do so by delegating to the embedded client.
func (c LoggingClient) Do(req *http.Request) (*http.Response, error) {
log.Printf("Sending request to %s", req.URL)
resp, err := c.Client.Do(req) // Delegate to the embedded client
log.Printf("Received response with status %s", resp.Status)
return resp, err
}
// You can now wrap any concrete client (like the default http.Client)
func main() {
var myClient Client = http.DefaultClient
myClient = LoggingClient{Client: myClient} // Now it's a logging client!
// myClient.Do() will now log.
}
This pattern is used everywhere in production Go code. It’s an incredibly clean way to implement middleware and decorators without complex inheritance hierarchies. You’re just wrapping one implementation inside another, and the embedding ensures the type signatures are correct. It’s brilliant, and once you get it, you’ll start seeing opportunities for it everywhere.