AlgoMaster Logo

Struct Embedding (Composition)

Last Updated: May 17, 2026

11 min read

A Customer has an Address. An Order has timestamps for when it was created and updated. A PremiumCustomer is everything a regular Customer is, plus a few extras. Writing those out as ordinary nested fields works, but it gets repetitive fast. Go has a cleaner mechanism for stitching one type's fields and methods into another: struct embedding. This lesson covers what embedding looks like, how field promotion works, when to use embedding over plain nesting, and the rules around name conflicts.

Embedding Syntax

Embedding means writing a named type as a field of a struct without giving it a field name. The type's name effectively becomes the field name, but the fields and methods of the embedded type are also accessible as if they belonged to the outer struct.

The line Address (with no field name) inside Customer is the embedding. Compare it to the normal nested form, which would be Addr Address or similar. The difference is small in the source, but it changes how you access the inner fields.

Notice the initializer. The embedded field is referred to by its type name, Address. That's the field name Go gives it automatically: the last segment of the type's name. So Address is the field name here, and inside the literal you write Address: Address{...}.

Field Promotion

Field promotion is the payoff. The fields of the embedded type can be accessed directly on the outer struct, without naming the embedded field.

Both c.City and c.Address.City return the same value. The promoted form (c.City) is what most code uses, because it reads naturally. The explicit form (c.Address.City) is occasionally useful when you want to make the structure obvious, or when you need to pass the embedded value as a whole to another function.

You can also assign through the promoted name, and the change reflects in the embedded value:

There's only one City in memory: the one inside the embedded Address. The promoted name is just a shortcut for reaching it.

Here's how the lookup actually works. When you write c.City, the compiler looks for a field named City directly on Customer. It doesn't find one. So it looks at each embedded type and finds City on Address. That's a depth-1 promotion: one hop from Customer into Address.

The diagram shows the resolution path. The outer struct is checked first; if the name isn't found there, each embedded type is searched at depth 1. The promoted access succeeds because exactly one embedded type has the field.

Embedding vs Nesting

Embedding is one option. Plain nesting (a named field of a struct type) is the other. The difference shows up in syntax, not in memory layout.

CustomerA and CustomerB hold the same data in memory. The only thing that differs is how you reach the inner fields. With nesting, you must write a.Addr.City. With embedding, both b.City and b.Address.City work.

Use embedding when the relationship is "is-a-kind-of" or when the outer type is essentially extending the inner type with extra fields. A Customer has an address but the address is so tied to the customer that promoting its fields reads naturally. An Order has audit info (created at, updated at), and you don't want to type order.Audit.CreatedAt everywhere.

Use plain nesting when the relationship is "has-a" with strong identity for the inner value, or when the field name carries meaning beyond the type. A user might have a HomeAddress and a ShippingAddress. Both are Address values, but they aren't interchangeable, so giving each a distinct field name is the right call.

StyleSyntaxAccessUse When
EmbeddingAddress (no name)c.City or c.Address.CityThe relationship reads as "is-a" or the outer type extends the inner
NestingAddr Addressc.Addr.City onlyThe field name carries meaning, or you need multiple fields of the same type

You can't embed two values of the same type in a single struct, because the type name would become two identical field names, which Go forbids. A struct with both HomeAddress and ShippingAddress must use named fields, not embedding.

That's a nesting design, not an embedding one. The structure is explicit, and that's what makes it correct.

Multiple Embeddings

A struct can embed more than one type at once. Each embedded type contributes its fields, and as long as no two embedded types have a field with the same name, all the fields get promoted.

Both Address and Timestamps got their fields promoted. The shopping order looks like a single flat struct from the outside (o.City, o.CreatedAt), while internally the data is grouped by purpose.

Embedded type names must be distinct at the top level. The "name" of an embedded type is the last segment of the type's name. So Address and pkg.Address would clash if you tried to embed both in the same struct, because both contribute the field name Address. The compiler rejects that with a duplicate field error.

You can also embed a struct that itself embeds other structs. A PremiumCustomer that embeds a Customer (which embeds an Address) gets two levels of promotion.

p.City walks two levels: first into Customer, then into Address. Go calls this depth-N promotion. Shallower fields win over deeper ones, which matters when names collide.

p.Name resolves at depth 1 because Name lives directly on Customer. p.City resolves at depth 2 because City lives one more level deep, inside Address. The promotion is automatic at any depth, but the search starts shallow and stops at the first match.

Name Conflicts

If two embedded types have a field (or method) with the same name at the same depth, promotion is ambiguous. Go doesn't try to guess. Accessing the conflicting name through the outer struct is a compile error, and you have to qualify it explicitly.

Both Address and Timestamps have a field named ID at depth 1. Writing o.ID is a compile error with the message ambiguous selector o.ID. The fix is to spell out which one you want: o.Address.ID or o.Timestamps.ID.

The non-conflicting fields still promote fine. o.City and o.CreatedAt work as before, because each lives on exactly one embedded type.

Depth matters in conflict resolution. If a field exists at depth 1 and another field with the same name exists at depth 2, the depth-1 field wins and no error is raised. The compiler resolves to the shallower one.

o.ID returns the field declared directly on Outer, not the one buried in Inner. The depth-2 field is shadowed but not deleted: you can still reach it through the explicit path. This is a useful pattern when you want to override a name inherited from an embedded type.

The rule for the compiler is straightforward: at each depth, count how many candidates have the requested name. If exactly one wins at the shallowest depth, that's the answer. If two or more tie at the shallowest depth, it's ambiguous and the compiler refuses to pick.

SituationWhat CompilesOutcome
Field exists on exactly one embedded typeo.CityResolves to that field
Same name on two embedded types at same deptho.IDCompile error: ambiguous selector
Same name at multiple depths, one shallowero.ID (the shallower one)Shallower wins, deeper is shadowed
Always works regardlesso.Address.ID, o.Inner.IDExplicit path bypasses any ambiguity

When you hit a conflict, you have two ways forward. Use the explicit path on the spot (o.Address.ID), or add a top-level field on the outer struct that shadows the conflict. Both are idiomatic; which to use depends on whether the shadowing makes sense as part of the type's design.

Embedding Pointer Types

You can embed a pointer to a type instead of the value. Both forms support field promotion, but they differ in how the embedded value is stored and shared.

Value embedding (Address) copies the Address into the Customer at construction time. Editing the original addr after that doesn't touch the customer's copy. Pointer embedding (*Address) stores only a pointer, so the customer and the original addr see the same underlying value.

There's a sharp edge with pointer embedding: if the pointer is nil, accessing a promoted field panics. With value embedding there's no way to have a nil embedded value, because the value is always there (the zero value if you didn't initialize it).

The fields and methods of an embedded pointer are still promoted, but the call goes through the pointer at runtime. If the pointer is nil, you panic. With value embedding you can't run into this.

When should you use which? Value embedding is the default. Reach for pointer embedding when the embedded type is large enough that copying it would cost (so you want to share rather than copy), or when you specifically want changes through the original to be visible through the outer struct. For embedding alone, prefer values unless you have a concrete reason not to.

Methods Are Promoted Too

This lesson focuses on fields. But it's worth seeing one example so you understand why Go developers call embedding "composition for behavior", not just "composition for data".

When you embed a type, the methods defined on that type are promoted as if they were methods of the outer type. The outer type doesn't have to redeclare them. Callers can invoke the method on the outer value, and it runs against the embedded value.

FullAddress is defined on Address, but the call c.FullAddress() works because Customer embeds Address. The Go runtime calls FullAddress with the embedded Address value as the receiver.

This is what people mean when they say Go favors composition over inheritance. There's no class hierarchy and no extends keyword. You compose types by embedding, and the methods that come with each embedded type show up automatically on the outer type. For now, take away that embedding promotes both fields and methods, and that's the design lever that makes embedding feel like inheritance even though it isn't.

Why Go Favors Embedding Over Inheritance

Embedding is composition: the outer struct has an inner value (or a pointer to one), and gets the convenience of promoted fields and methods on top. It's not subtype inheritance. A Customer that embeds Address is not a kind of Address. You can't pass a Customer to a function expecting an Address parameter. You can pass c.Address, but the outer type and the inner type stay distinct.

That distinction is the point. Inheritance hierarchies tend to grow tightly coupled. A change to a base class ripples through every subclass. Method dispatch goes through the hierarchy, which makes the actual code path harder to predict. Embedding sidesteps both problems. There's no hierarchy, just one struct holding another. Promotion is a syntactic convenience, not a runtime mechanism. If you want to know what c.FullAddress() does, you look at Address.FullAddress, and the call site is doing exactly that: invoking that method on c.Address.

The trade-off is that embedding doesn't give you polymorphism by itself. Go uses interfaces for that. The combination, embedding for code reuse plus interfaces for substitution, is Go's answer to most of what class hierarchies do in other languages.

A useful way to think about it: when you embed type T into struct S, you're saying "an S always has a T inside it, and I want the fields and methods of T to read like they're on S for convenience." You're not saying "an S is a T". The compiler enforces that distinction, and the result is loose coupling: you can change T without affecting S as long as the names you use through promotion stay stable.

Common Mistakes

Treating embedding as inheritance. A Customer that embeds Address is not assignable to an Address variable. A function func ship(a Address) will not accept a Customer. You have to pass c.Address. Embedding promotes fields and methods, not type identity.

Forgetting that name conflicts disable promotion. Two embedded types with overlapping field names don't merge their fields. Promotion is silently disabled for the conflicting name, and any unqualified access becomes a compile error. The fix is to qualify the access (o.Address.ID), not to argue with the compiler.

Confusing embedding with nesting. Address Address and Address look similar in struct definitions but behave differently. The first is a named field of type Address (no promotion), the second is embedded (with promotion). The presence or absence of a field name before the type is what flips the switch.

C1 and C2 have the same memory layout but very different access patterns. c1.City is a compile error; c2.City works.

Embedding a pointer and forgetting to initialize it. A Customer that embeds *Address has a nil pointer if you construct the customer without setting the field. Reading any promoted field through that nil pointer panics at runtime. Always initialize the pointer, or use value embedding instead.

Trying to embed two types with the same last-segment name. If you import two packages that both define an Address type, you can embed only one of them in a single struct. The other has to be added as a named field with a distinct name.

Summary

  • Embedding means writing a named type as a field of a struct with no field name. The type's last-segment name becomes the field's name.
  • Field promotion lets you access embedded fields directly on the outer struct (c.City). The explicit path (c.Address.City) also still works.
  • Embedding and named-field nesting produce the same memory layout. Use embedding when promoted access reads naturally; use nesting when the field name carries meaning or you need multiple fields of the same type.
  • A struct can embed multiple types. As long as their field names don't collide at the same depth, every field promotes.
  • If two embedded types share a field name at the same depth, that name's promotion is disabled and unqualified access is a compile error. Shallower fields shadow deeper ones, which lets the outer struct override an inherited name.
  • Embedding a pointer (*Address) shares state with whatever else holds the pointer, but accessing promoted fields through a nil pointer panics. Prefer value embedding unless you have a concrete reason for sharing.
  • Methods are promoted along with fields, which is what makes embedding feel like inheritance even though it isn't. Go favors composition because it stays loosely coupled and the call paths stay easy to follow.

The next lesson, Struct Tags, looks at the backtick strings you can attach to struct fields, which carry metadata that packages like encoding/json use to map field names to external formats.