skip to content

In Go, what changes when a struct embeds a pointer type or an interface instead of a struct value?

level: middleimportance: nice to knowfreq 38%

answer

  1. the field name is still the type name
  2. what does the zero value hold
  3. nil until something constructs it
  4. an interface field brings its whole method set
  5. wrap wide interfaces, write one method

basics

~20 s

Promotion works the same, but the zero value does not: an embedded pointer or interface field starts nil, so a promoted call panics until you set it. Embedding an interface also makes the outer type satisfy that interface with no code.

solid answer

~50 s

Selectors behave identically — the field is still named after the type, so `*Grid` gives a field named `Grid`, and `d.Rows` or `d.Draw()` still reach through it. What changes is initialisation and intent. An embedded struct value is usable at its zero value; an embedded `*Grid` or `io.Reader` starts nil, so the first promoted call is a nil dereference or a call on a nil interface, and the outer type is only usable once you construct it properly. Embedding a pointer lets several outer values share one instance or leave it absent. Embedding an interface is the wrapper idiom: the outer type satisfies the interface immediately because the interface's methods are promoted, so you declare only the one or two methods you want to intercept and everything else forwards to the wrapped value. Note you can embed `*T` or an interface type, but never a pointer to an interface.

code

go · 18 lines
go
type Grid struct{ Rows int }

func (g *Grid) Resize(rows int) { g.Rows = rows }

type Dashboard struct {
	*Grid // field is named Grid; nil in the zero value
	Title string
}

func broken() {
	var d Dashboard
	d.Resize(24) // panics: nil pointer dereference
}

func ok() {
	d := Dashboard{Grid: &Grid{}}
	d.Resize(24)
}

go deeper

for a junior

Know that you can embed *T or an interface as well as a plain struct, and that in those cases the field starts nil so something has to set it before any promoted call.

for a middle

Explain that promotion and the implicit field name are unchanged, and that the real difference is the zero value plus the ability to satisfy an interface without writing its methods.

for a senior

Show that you treat a type embedding a pointer or an interface as constructor-only, and that you can spot the nil-dereference panic whose cause is an embedded field nobody set.

for a principal

Decide as a policy whether wrapper types in your codebase embed an interface for brevity or forward explicitly for safety, given that the embedded form silently absorbs future methods of that interface.

## The selector rules do not change An embedded field may be a type name `T`, a pointer `*T` to a non-interface type, or an interface type. In every case the implicit field name is the unqualified type name — `*Grid` gives a field named `Grid`, `io.Reader` gives a field named `Reader` — and promotion works exactly as it does for an embedded struct value. `d.Rows` on a struct embedding `*Grid` compiles to a pointer dereference plus a field access, just as `d.Grid.Rows` would. What differs is what the zero value means, and what you are saying about the relationship. ## Embedding a pointer: the zero value is a trap ```go type Grid struct{ Rows int } func (g *Grid) Resize(rows int) { g.Rows = rows } type Dashboard struct { *Grid Title string } var d Dashboard d.Resize(24) // panics: the embedded *Grid is nil ``` With an embedded `Grid` value, `var d Dashboard` is immediately usable — the inner struct is zeroed in place. With `*Grid` the field is nil, so the first promoted access dereferences nil and panics at run time. Nothing at compile time warns you, which makes this a genuine production bug rather than a curiosity: a struct that used to embed a value and was changed to embed a pointer keeps compiling and starts panicking on whatever path forgot to construct it. Why do it anyway? Three honest reasons: - **Sharing.** Several outer values can embed the same `*Grid` and see each other's mutations. - **Optionality.** A nil embedded pointer is a meaningful absent, if every promoted use is guarded. - **Size and copying.** The outer struct carries one word instead of the whole embedded struct. A related detail: an embedded `*T` promotes the methods declared with receiver `T` and with receiver `*T` alike, so `Resize` above is reachable even from a non-addressable `Dashboard`, whereas an embedded value would only reach a pointer-receiver method through an addressable outer value. ## Embedding an interface: implementation for free ```go type countingReader struct { io.Reader // promotes Read; the type satisfies io.Reader immediately n int64 } func (c *countingReader) Read(p []byte) (int, error) { n, err := c.Reader.Read(p) c.n += int64(n) return n, err } ``` The embedded interface field contributes its whole method set to the outer type, so `countingReader` satisfies `io.Reader` before you write a line. You then declare only the methods you want to handle yourself; at depth zero they shadow the promoted ones, and inside them you reach the wrapped value through the field's implicit name, `c.Reader`. This is the standard decorator shape in Go, and it scales to wide interfaces: to intercept one method of a twelve-method interface you declare one method instead of twelve forwarders. The cost is that the compiler no longer protects you. The eleven methods you did not write are promoted from a field that may be nil, and if it is, calling one panics on a nil interface. The type checker cannot tell the difference between `countingReader{Reader: f}` and `countingReader{}`. A second, deliberate use of the same trick is a partial test double: embed the interface, leave it nil, implement only the methods the test exercises, and let any unexpected call panic loudly rather than silently returning a zero value. ## What you may not embed An embedded field must be written as a type name `T` or as `*T` where `T` is a non-interface type name, and `T` itself may not be a pointer type. So: - `io.Reader` — legal, embeds the interface. - `*Grid` — legal. - `*io.Reader` — illegal; you cannot embed a pointer to an interface. There is almost never a reason to want one either, since an interface value is already a reference-like two-word value. - A defined type whose underlying type is a pointer — illegal as an embedded field. ## Choosing between the three Embed a **value** when the inner data is genuinely part of the outer thing and the zero value should work. Embed a **pointer** when the inner thing is shared, optional, or large, and accept that construction becomes mandatory. Embed an **interface** when you are wrapping something you do not own and want to intercept a subset of its behaviour — and when you do, treat the constructor as the only supported way to build the type, because the zero value of that type is a panic waiting for a caller.

  • Why does embedding an interface let you implement only one of its methods?
    The embedded interface field contributes the entire method set to the outer type by promotion, so the type satisfies the interface before you write anything. A method you declare on the outer type sits at depth zero and shadows the promoted one. The rest still dispatch to the wrapped value at run time, and panic if that field is nil.
  • Can you embed a pointer to an interface, such as *io.Reader?
    No. An embedded field must be a type name or a pointer to a non-interface type name, so `*io.Reader` is rejected. It would also be pointless: an interface value already holds a reference to the dynamic value, and a pointer to it only adds a level of indirection nobody wants.
  • What is the practical risk of switching an embedded value field to an embedded pointer?
    Every call site keeps compiling, because the promoted selectors are unchanged, but the zero value stops working. Any path that built the outer struct without setting the embedded pointer now panics on first use. If you make that change, add a constructor and treat the zero value as invalid.

saying these in an interview costs you the question

  • Thinks an embedded pointer is allocated automatically
  • Expects a compile error rather than a nil panic
  • Says embedding an interface requires writing every method
  • Believes the field name changes to include the star
  • Claims a pointer to an interface can be embedded
  • Assumes the zero value of a wrapper type is usable