Struct Embedding

Go doesn’t have classical inheritance the way Java or C++ do — there’s no extends keyword and no class hierarchy to climb. Instead, Go gives you struct embedding: placing one type inside another without giving it a field name, so the outer type automatically gains access to the inner type’s fields and methods. This is Go’s primary mechanism for code reuse, and it shows up constantly in the standard library and in idiomatic Go code, so understanding exactly what it does — and what it deliberately does not do — is essential.

Overview: How Struct Embedding Works

When you declare a field in a struct using only a type name, with no identifier in front of it, that field is called an anonymous field, and the struct is said to embed that type. The field’s implicit name is the type’s own name (its unqualified name, ignoring any package prefix), so you can still refer to it explicitly when you need to.

type Engine struct {
	Horsepower int
}

type Car struct {
	Engine // embedded field, implicit name "Engine"
	Model  string
}

Here Car embeds Engine. Every exported (and unexported, within the same package) field and method of Engine becomes accessible directly on a Car value — this is called promotion. So car.Horsepower works even though Car never declared a Horsepower field itself; the compiler rewrites it as car.Engine.Horsepower behind the scenes. The same happens for methods: if Engine had a method Start(), then car.Start() would call car.Engine.Start() automatically.

It’s important to be precise about what’s happening under the hood, because it’s easy to mistake this for inheritance. A Car is not an Engine — there’s no type hierarchy and no is-a relationship enforced by the compiler. Car simply contains an Engine value as one of its fields, and the compiler generates convenience selectors so you don’t have to type car.Engine.Horsepower every time. This is composition, dressed up with syntax that makes the common case terse. The distinction matters most when you consider method dispatch: a method defined on Engine only ever sees Engine‘s own methods, never any method that Car later defines with the same name. There is no virtual dispatch — we’ll see exactly why that trips people up in the Common Mistakes section.

You can embed:

  • A struct type, by value (Engine) — the embedded struct is copied into the outer struct’s memory layout.
  • A pointer to a struct type (*Engine) — the outer struct stores a pointer, so the embedded value can be shared, can be nil, and mutations through promoted pointer-receiver methods are visible to anyone else holding the same pointer.
  • An interface type — the outer struct promotes whatever method set the interface defines, and calling a promoted method dispatches to whatever concrete value is currently stored in that interface field at runtime. This is how types like bufio.ReadWriter are built by embedding a reader and a writer.

Field and method promotion is resolved at compile time using a shallowest-match-wins rule: Go looks at the outer struct’s own fields and methods first, then one level of embedding, then two levels, and so on, stopping at the first depth where it finds a unique match. If two embedded types at the same depth both have a field or method with the same name, and you try to use it without qualifying which one you mean, the compiler refuses to guess — you get an ambiguous selector error, and you must disambiguate explicitly.

Syntax

type Outer struct {
    Inner              // embedded value type -- field name is "Inner"
    *PointerType       // embedded pointer type -- field name is "PointerType"
    SomeInterface       // embedded interface -- field name is "SomeInterface"
    RegularField Type  // an ordinary, named field for comparison
}
Form Meaning
Inner Embeds a struct (or any named type) by value; fields/methods are promoted.
*Inner Embeds a pointer to a struct; the field can be nil, and calling a promoted method through a nil pointer panics unless the method is written to tolerate a nil receiver.
SomeInterface Embeds an interface type; the outer type gets that interface’s method set promoted, dispatched dynamically to whatever concrete value the field holds.
Outer{}.Inner The explicit, always-valid way to reach the embedded field, useful for disambiguation or initialization.

Examples

Example 1: Basic field and method promotion

package main

import "fmt"

type Animal struct {
	Name string
}

func (a Animal) Describe() string {
	return "Animal: " + a.Name
}

type Dog struct {
	Animal
	Breed string
}

func main() {
	d := Dog{
		Animal: Animal{Name: "Rex"},
		Breed:  "Labrador",
	}
	fmt.Println(d.Name)
	fmt.Println(d.Describe())
	fmt.Println(d.Breed)
}

Output:

Rex
Animal: Rex
Labrador

Notice the struct literal: even though Name is promoted, you still initialize the embedded field using its type name as the key (Animal: Animal{Name: "Rex"}) — promotion only affects how you read fields and call methods afterward, not how you build the literal. Reading d.Name and calling d.Describe() both work directly on the Dog value because Dog embeds Animal at depth one.

Example 2: Shadowing a promoted method

package main

import "fmt"

type Base struct {
	ID int
}

func (b Base) Describe() string {
	return fmt.Sprintf("Base#%d", b.ID)
}

type Wrapper struct {
	Base
	Label string
}

func (w Wrapper) Describe() string {
	return fmt.Sprintf("%s (%s)", w.Base.Describe(), w.Label)
}

func main() {
	w := Wrapper{Base: Base{ID: 42}, Label: "special"}
	fmt.Println(w.Describe())
	fmt.Println(w.Base.Describe())
}

Output:

Base#42 (special)
Base#42

Because Wrapper declares its own Describe method, that method exists at depth zero and wins over the promoted Base.Describe at depth one — this is exactly the shallowest-match-wins rule. Wrapper.Describe can still reach the shadowed method explicitly through w.Base.Describe(), which is the standard way to build on top of an embedded type’s behavior instead of only replacing it.

Example 3: Embedding a pointer for shared, mutable state

package main

import "fmt"

type Counter struct {
	count int
}

func (c *Counter) Increment() {
	c.count++
}

func (c *Counter) Value() int {
	return c.count
}

type Stats struct {
	*Counter
	Name string
}

func main() {
	s := Stats{Counter: &Counter{}, Name: "hits"}
	s.Increment()
	s.Increment()
	s.Increment()
	fmt.Println(s.Name, s.Value())
}

Output:

hits 3

Stats embeds *Counter, a pointer. The promoted Increment and Value methods have pointer receivers, so calling s.Increment() is shorthand for s.Counter.Increment(), which mutates the single Counter that s.Counter points to. If you shared that same *Counter pointer with another struct, both would see each other’s increments — embedding a pointer, unlike embedding a value, means the embedded state is shared, not copied.

How It Works Step by Step

When the compiler sees a selector expression like s.Value(), it performs this resolution:

  1. Look for a field or method literally named Value declared directly on Stats. None exists.
  2. Descend one level into each embedded field (*Counter here) and look for Value there. Counter has a Value() method, so it’s found at depth one.
  3. If more than one embedded type at the same depth had matched, the compiler would stop and report an ambiguous selector instead of guessing — you’d have to write s.Counter.Value() explicitly.
  4. Once resolved, the compiler rewrites the call as s.Counter.Value() and, since Value has a pointer receiver, uses the pointer stored in s.Counter directly.

This resolution happens entirely at compile time — there’s no runtime lookup table walking a class hierarchy. That’s also why a method defined on the embedded type can never call back into an overriding method defined on the outer type: at the point Counter.Increment is compiled, the compiler only knows about Counter; it has no idea Stats even exists, let alone that Stats might someday embed it.

Common Mistakes

Mistake 1: Ambiguous selectors from same-depth embedding

If two embedded types at the same depth both define a field or method with the same name, referencing it without qualification is a compile error:

type A struct {
	Name string
}

type B struct {
	Name string
}

type C struct {
	A
	B
}

func main() {
	c := C{A: A{Name: "a"}, B: B{Name: "b"}}
	fmt.Println(c.Name) // compile error: ambiguous selector c.Name
}

The fix is to qualify the selector with the embedded type’s name so the compiler knows exactly which one you mean:

package main

import "fmt"

type A struct {
	Name string
}

type B struct {
	Name string
}

type C struct {
	A
	B
}

func main() {
	c := C{A: A{Name: "a"}, B: B{Name: "b"}}
	fmt.Println(c.A.Name, c.B.Name)
}

Output:

a b

Mistake 2: Expecting virtual dispatch like in Java or C#

This is the mistake that catches people coming from class-based languages hardest. It compiles fine, but doesn’t do what you’d expect:

package main

import "fmt"

type Base struct{}

func (b Base) Name() string {
	return "Base"
}

func (b Base) Greet() string {
	return "Hello, " + b.Name()
}

type Outer struct {
	Base
}

func (o Outer) Name() string {
	return "Outer"
}

func main() {
	o := Outer{}
	fmt.Println(o.Greet())
}

Output:

Hello, Base

A Java or C# programmer would expect Greet to call the overriding Outer.Name, printing “Hello, Outer” — but Go embedding is composition, not inheritance, and there’s no dynamic dispatch through it. Base.Greet was compiled knowing only about Base; it calls Base.Name, full stop, regardless of what any struct embedding Base later defines. The fix is to not rely on this pattern: put the method that needs the current type’s behavior directly on the outer type, and have it call the pieces it needs explicitly.

package main

import "fmt"

type Base struct{}

func (b Base) Name() string {
	return "Base"
}

func (b Base) greetWith(name string) string {
	return "Hello, " + name
}

type Outer struct {
	Base
}

func (o Outer) Name() string {
	return "Outer"
}

func (o Outer) Greet() string {
	return o.greetWith(o.Name())
}

func main() {
	o := Outer{}
	fmt.Println(o.Greet())
}

Output:

Hello, Outer

Outer now owns Greet, so it resolves at depth zero and its body calls o.Name(), which correctly picks up Outer‘s own method. If you genuinely need runtime-polymorphic behavior, model it with an interface and a field of that interface type instead of leaning on struct embedding to fake it.

Best Practices

  • Use embedding to reuse behavior and data through composition, not to simulate a class hierarchy — think has-a with convenience, not is-a.
  • When you embed two or more types that might share a field or method name, be ready to disambiguate with outer.TypeName.Field; don’t rely on only one of them ever adding that name.
  • Prefer a regular, named field over embedding when you don’t actually want the inner type’s whole method set promoted onto the outer type’s public API — embedding is not just for saving keystrokes.
  • Initialize pointer-typed embedded fields before calling their promoted methods; a nil embedded pointer used through a promoted pointer-receiver method panics just like any other nil pointer dereference.
  • Embed interfaces to compose small interfaces into bigger ones, rather than redeclaring every method signature by hand.
  • Keep embedding shallow. Embedding a struct that itself embeds another struct multiple levels deep makes promoted-method resolution and error messages harder to follow — favor one level whenever possible.
  • Document embedded fields in exported types; because promoted members don’t show up as ordinary field declarations, readers and tools benefit from a short comment explaining what’s being reused and why.

Practice Exercises

  • Define a Person struct with Name string and Age int, then an Employee struct that embeds Person and adds Salary float64. Construct an Employee, and print the promoted Name field together with Salary in one fmt.Println call.
  • Create two structs, Logger and Cache, each with its own Report() string method. Embed both into a Service struct and try calling service.Report() — note the compiler error you get, then fix it by calling service.Logger.Report() and service.Cache.Report() explicitly.
  • Write a Box struct that embeds *Counter (reusing the Counter type from Example 3). Create two Box values that share the same *Counter pointer and confirm, by incrementing through one and reading through the other, that the count is shared — then create a third Box with its own fresh *Counter and confirm its count stays independent.

Summary

  • Struct embedding places one type inside another as an anonymous field, whose implicit name is the type’s own name.
  • Embedded fields and methods are promoted onto the outer type, so you can call them directly without an extra selector — but you can always fall back to the qualified form (outer.Inner.Field).
  • Embedding is composition, not inheritance: there’s no is-a relationship and, critically, no virtual or dynamic dispatch when an embedded type’s method calls another method on itself.
  • You can embed a value, a pointer, or an interface; pointer and interface embedding allow shared or dynamically-dispatched state, while value embedding copies the inner struct into the outer one.
  • Promotion resolves by depth at compile time — the shallowest unique match wins, and same-depth conflicts between two embedded types produce an ambiguous-selector compile error that you resolve by qualifying the selector explicitly.
  • Use embedding deliberately for genuine code reuse, keep it shallow, and reach for interfaces (not embedding tricks) when you actually need runtime polymorphism.