skip to content

In Go, what initializes the embedded part of a struct when there is no base-class constructor?

level: middleimportance: nice to knowfreq 33%

answer

  1. there are no constructors in Go
  2. every field starts at its zero value
  3. an embedded field is just a field
  4. useful zero values replace the base constructor
  5. your own New has to build the embedded part

basics

~20 s

Nothing runs. Creating a struct zeroes every field, embedded ones included, so the embedded part gets its zero value. If it needs setup, the outer type's own New function must do it or the composite literal must supply it.

solid answer

~50 s

Go has no constructor at all, so there is no chain to run: allocating a struct sets every field to its zero value, and a composite literal fills in only what you name. The embedded field is treated exactly like any other field. That is why Go types are designed so the zero value is useful — an embedded `sync.Mutex` or `bytes.Buffer` is ready without setup. When the embedded type needs real initialization, the outer type's own `New` function has to do it: either build the embedded value with its own constructor and assign it, or set the fields directly. The failure mode to name is a zero-valued embedded part that looks fine: an unmade map panics on the first write, an embedded pointer type is nil so promoted selectors panic, and an embedded interface is nil so calls through it panic. None of that shows up until the call happens.

code

go · 15 lines
go
type baseReconciler struct {
	state map[string]string
}

func (b *baseReconciler) record(k, v string) { b.state[k] = v }

type NodeReconciler struct {
	baseReconciler
}

// var n NodeReconciler; n.record("a", "b") panics: the embedded
// map is still nil because nothing initialized the embedded field.
func NewNodeReconciler() *NodeReconciler {
	return &NodeReconciler{baseReconciler{state: make(map[string]string)}}
}

go deeper

for a junior

Remember that Go has no constructors: creating a struct zeroes every field, embedded ones included, and a composite literal only sets the fields you name.

for a middle

Explain what a zero value is field by field, why an embedded sync.Mutex needs nothing while an embedded map field must be made, and how you set the embedded field with its type name as the key in a keyed literal.

for a senior

Show what you do about it in a shared package: unexported fields that need setup, a documented New, and a test asserting how the zero value fails, so nobody ships a value whose embedded part was never initialized.

for a principal

Push the design decision upstream: types whose zero value is usable can be embedded freely by other teams, while types that require a constructor constrain every consumer, so treat useful zero values as an API-surface choice, not a style preference.

## There is no constructor to chain Go has no constructors, so the question of what runs for the base part has a short answer: nothing. Creating a value — `var n NodeReconciler`, `new(NodeReconciler)`, a composite literal, an element of a freshly made slice — sets the whole thing to the zero value of its type, recursively: numeric fields to 0, strings to "", pointers, interfaces, maps, slices, channels and function values to nil, and struct fields to their own zero values. An embedded field is a field, so it is zeroed like the rest. Then, if you wrote a composite literal, the fields you named are assigned. No hook, no per-type initializer, no ordering rules to memorise. ## Why Go can live without one The convention that replaces the constructor is **make the zero value useful**. `sync.Mutex` is ready to lock the moment it exists, `bytes.Buffer` is an empty buffer, `strings.Builder` is ready to write to. A struct that embeds any of them needs no setup for that part at all, which is precisely why they are commonly embedded. Designing your own types the same way removes the need for a constructor and makes them safe to embed. When a type genuinely cannot have a useful zero value — it needs an open connection, a made map, a running goroutine, a configured field — the convention is an exported `NewT` function returning `T` or `*T`. That function is ordinary code and nothing calls it implicitly. ## What that means for an outer type If `NodeReconciler` embeds a `baseReconciler` that holds a map, `var n NodeReconciler` gives you a `baseReconciler` whose map is nil. Reading from a nil map is fine and yields the zero value with `ok == false`; writing to one panics. So the promoted method that records state panics on first use, and it panics inside the embedded type's code, which makes the stack trace point away from the real defect — the constructor that was never written. The outer type's constructor must therefore initialize the embedded part explicitly. Two shapes are common: assign the embedded field from the embedded type's own constructor (`n.baseReconciler = *newBase()` or use a pointer field), or fill it in a keyed composite literal (`&NodeReconciler{baseReconciler: baseReconciler{state: make(map[string]string)}}`). The keyed form uses the type name as the field key, since that is the embedded field's name. ## Two sharper variants of the same trap **Embedding a pointer type.** `type NodeReconciler struct{ *baseReconciler }` starts as a nil pointer. Every promoted field selector and every promoted method call dereferences it, so the zero value panics on essentially any use. It looks especially like inheritance and behaves least like it. **Embedding an interface.** `type NodeReconciler struct{ Differ }` starts as a nil interface. The outer type satisfies `Differ` by promotion and compiles happily, and the first call through it panics. This is the pattern used to give a base a callback hole, and forgetting the wiring is its standard failure. ## Documenting the requirement Because the language cannot force construction, the type has to say so. Keep the fields that need setup unexported so a caller in another package cannot produce a valid-looking literal, put the requirement in the doc comment of the type, and add a test that constructs the zero value and asserts the failure you expect — that test is the executable version of "this type must be built with NewNodeReconciler". ## The interview-shaped summary Zero values all the way down; composite literals set what you name; there is no chain and no super; Go compensates by making zero values useful, and where it cannot, by convention-level `New` functions that the outer type must call itself.

  • Why is an embedded sync.Mutex fine without any initialization?
    Because its zero value is an unlocked mutex, ready to use. Go's convention is to make the zero value meaningful precisely so that composing types needs no setup step; the same is true of bytes.Buffer and strings.Builder.
  • What changes when the embedded field is a pointer type rather than a value?
    The zero value is a nil pointer, so every promoted field access and promoted method call dereferences nil and panics. The type becomes unusable until something assigns the field, which makes a constructor mandatory rather than merely convenient.
  • How do you set the embedded field in a composite literal?
    Use the type name as the key, since that is the embedded field's name: NodeReconciler{baseReconciler: baseReconciler{state: make(map[string]string)}}. A positional literal works too, but only if you supply every field of the outer struct in order.
  • How do you stop callers from creating an unconstructed value of such a type?
    You cannot forbid it, so you make it hard and loud: keep the fields that need setup unexported so another package cannot fill them in a literal, document the constructor requirement on the type, and add a test that asserts the zero value fails the way you expect.

saying these in an interview costs you the question

  • Expects an embedded type's New function to run automatically
  • Thinks a struct literal allocates maps or channels for you
  • Assumes an embedded pointer field is allocated on use
  • Looks for constructor ordering rules Go does not have
  • Treats an embedded interface field as always non-nil