skip to content

In Go, does embedding a struct make the outer type usable wherever the embedded type is expected?

level: juniorimportance: must knowfreq 70%

answer

  1. composition, not a class hierarchy
  2. the embedded field has a name
  3. the outer type stays its own type
  4. no conversion between the two types
  5. reach for n.StateRecord by name

basics

~10 s

No. Embedding is composition, not subtyping: a struct that embeds StateRecord is its own distinct type and is never assignable to StateRecord. You pass the embedded value explicitly, as n.StateRecord.

solid answer

~40 s

No. An embedded field is just a field whose name is its type name, so `NodeState{StateRecord, Node string}` has a `StateRecord` field you can reach as `n.StateRecord`. What you get for free is spelling: promoted field and method names you can write directly on the outer value. What you do not get is substitutability — `NodeState` and `StateRecord` are different types, there is no implicit conversion and no explicit one either, so `record(n)` where `record` takes a `StateRecord` simply does not compile. The same holds at runtime: an interface value holding a `NodeState` has dynamic type `NodeState`, so a type assertion to `StateRecord` fails. If you want one function to accept both, declare an interface with the behaviour you need; interface satisfaction, not embedding, is where Go's polymorphism lives.

code

go · 15 lines
go
type StateRecord struct {
	Desired, Actual string
}

type NodeState struct {
	StateRecord
	Node string
}

func record(s StateRecord) { /* ... */ }

func call(n NodeState) {
	// record(n) does not compile: NodeState is not a StateRecord.
	record(n.StateRecord) // pass the embedded value explicitly
}

go deeper

for a junior

Be ready to state it in one sentence: embedding gives you the embedded type's fields and methods by name, but never makes the outer value pass as the embedded type. Have the failing call and the n.StateRecord fix ready.

for a middle

Explain the mechanism: the embedded field's name is its type name, so n.Desired is rewritten to n.StateRecord.Desired, and the two named types have different underlying types, so no conversion exists in either direction.

for a senior

Show how you keep an API honest under this rule: accept a small interface both types satisfy, or take the record explicitly and let callers pass the field, rather than scattering type assertions that will fail.

for a principal

Own the coupling this creates in a package other teams import. An exported embedded struct becomes part of your surface, callers reach through it by name, and removing it later is a breaking change even though it looked like an internal reuse decision.

## What embedding actually declares Writing a type name inside a struct with no field name declares an **embedded** (anonymous) field: ```go type StateRecord struct { Desired, Actual string } type NodeState struct { StateRecord Node string } ``` `NodeState` has exactly two fields: one named `StateRecord` (its name is the unqualified type name) and one named `Node`. `n.StateRecord` is an ordinary field access, and `n.Desired` is shorthand the compiler resolves to `n.StateRecord.Desired`. That shorthand is the entire benefit of embedding over a plain named field. ## Why that is not subtyping Assignability in Go is a rule about types, and `NodeState` and `StateRecord` are two distinct named struct types with different underlying types — one has an extra field. No implicit conversion exists between them, and no explicit conversion exists either, because their underlying types are not identical. So a function declared as `func record(s StateRecord)` cannot be called with a `NodeState`; the compiler rejects it. You write `record(n.StateRecord)`, which passes a copy of the embedded value. The same is true of pointers: `*NodeState` is not a `*StateRecord`, though `&n.StateRecord` is one, and it points into the outer value. ## Runtime behaves the same way Put a `NodeState` into an `any` and the dynamic type recorded in the interface value is `NodeState`. A type assertion to `StateRecord` fails (`v, ok := x.(StateRecord)` gives `ok == false`; the one-result form panics). There is no class hierarchy to walk, no runtime notion of a base part that an assertion can reach. You assert to `NodeState` and then take the field. ## Where the polymorphism went Go does have a mechanism for writing one function that accepts many concrete types: interfaces. If both `StateRecord` and `NodeState` have a `Key() string` method, both satisfy `interface{ Key() string }` and both can be passed to a function taking that interface. Embedding can help the outer type acquire the method by promotion, but the substitutability comes from the interface, never from the embedding. This is the mental switch engineers coming from class-based languages have to make: the reuse mechanism (embedding) and the polymorphism mechanism (interfaces) are separate, and one does not imply the other. ## A concrete way to see it Suppose a controller stores desired-versus-actual state records and you introduce `NodeState` for node-specific reconciliation. Every helper you already wrote against `StateRecord` — logging, diffing, persistence — keeps compiling, but not one of them accepts a `NodeState`. You have three honest options: pass `n.StateRecord` at each call site; change the helpers to take a small interface both types satisfy; or keep the record as a plain named field (`Record StateRecord`) so nobody is tempted to read the declaration as inheritance in the first place. Embedding is the right call only when you actually want the promoted names. ## What embedding does and does not buy It buys: promoted field selectors, promoted methods on the outer type's method set, and a compact declaration. It does not buy: assignability, a successful type assertion, a constructor that runs for the embedded part, protected-style access (the embedded type's unexported fields are still invisible from another package), or dispatch from the embedded type's code back into the outer type. Each of those absences is a deliberate design choice, and each one is a place where an engineer expecting inheritance gets surprised. ## How to talk about it in an interview Say the sentence plainly — embedding is composition with a naming shortcut — then give the one-line proof: the call that does not compile, and the `n.StateRecord` that does. That answer signals you have written Go rather than translated another language into it.

  • So how do you write one function that accepts both the outer struct and the struct it embeds?
    Declare an interface holding the behaviour you need and take that as the parameter. If both types have the method, both satisfy it and both can be passed. The substitutability comes from interface satisfaction, which Go checks structurally at the call site, not from the embedding.
  • An interface value holds a NodeState. What does asserting it to StateRecord give you?
    It fails. The dynamic type stored in the interface is NodeState, and an assertion matches that type exactly. You assert to NodeState and then take the embedded field: `n, ok := v.(NodeState); rec := n.StateRecord`. There is no base part an assertion can reach on its own.
  • Does embedding a pointer, like *StateRecord, change the assignability answer?
    No. The outer type is still a distinct type; only the embedded field's type changed from a value to a pointer. You gain a shared, mutable record and a new hazard — the embedded pointer's zero value is nil, so promoted selectors panic until something assigns it.

saying these in an interview costs you the question

  • Says the outer struct is-a embedded type, like a subclass
  • Expects an implicit conversion from outer to embedded
  • Thinks a type assertion to the embedded type succeeds
  • Calls embedding inheritance with a different keyword
  • Assumes the embedded type's unexported fields become visible