skip to content

Go has no `extends` keyword - how do you reuse another type's behaviour inside a struct?

level: juniorimportance: must knowfreq 78%

answer

  1. reuse is a field, not a keyword
  2. the field name is the whole difference
  3. no field name means methods are promoted
  4. a named field forwards only what you choose

basics

~20 s

Go gives three choices: embed the type anonymously so its methods are promoted onto yours, hold it in a named field and forward only the calls you want, or depend on an interface it already satisfies.

solid answer

~50 s

There is no inheritance in Go, so reuse is written as a field. If I write the type name with no field name - `type Server struct { Logger }` - that is embedding: every method of `Logger` becomes callable as `s.Log(...)` and joins `Server`'s own method set, so `Server` also satisfies any interface `Logger` satisfied. If I give the field a name - `logger Logger` - nothing is promoted; callers reach it as `s.logger.Log(...)` and I hand-write forwarding methods for whatever I want on my own surface. The third option is not a field at all: declare a parameter or field of an interface type, and any value with the right methods fits, because satisfaction is implicit. Embedding is the least typing and the widest commitment; a named field is more typing and keeps the surface mine.

code

go · 14 lines
go
type Logger struct{ prefix string }

func (l Logger) Log(msg string) {}

type Cache struct{}

func (c *Cache) Get(k string) (string, bool) { return "", false }

type Server struct {
	Logger // embedded: Log is promoted onto Server
	cache *Cache // named: reached only as s.cache.Get(...)
}

// s.Log("started") compiles; s.Get("k") does not.

go deeper

for a junior

Be ready to name the three forms - embedded field, named field, interface - and to say that the missing field name is what turns an ordinary field into an embedding.

for a middle

Expect to explain that promotion puts the embedded type's methods into the outer type's method set, so the outer type silently satisfies interfaces the inner one satisfied.

for a senior

Show that you treat the choice as an API decision rather than a style one: what you embed, you publish. Expect a follow-up about what you would do differently in a package other teams import.

for a principal

Own the rule for the codebase: where embedding is allowed, where hand-written forwarding is mandatory, and how you stop a reviewer having to relitigate it on every pull request.

## Reuse in a language with no base classes Go has no classes, no `extends`, no `super` and no base-class constructors. Behaviour is attached to types by declaring methods on them, and reuse of *someone else's* behaviour is always spelled out as data: you put the other type inside your struct, or you accept it through an interface. There are exactly three concrete forms, and picking between them is one of the first real design decisions a Go author makes. ### 1. An embedded field Write the type with no field name: ```go type Server struct { Logger } ``` `Logger` is both the type and the field name here, so `s.Logger` is a valid selector. What separates this from an ordinary field is **promotion**: every method in `Logger`'s method set becomes callable on `Server` as `s.Log(...)`, and joins `Server`'s *own* method set. That last part is the consequential one. Interface satisfaction in Go is structural and implicit - a type satisfies an interface simply by having the right methods, with no declaration anywhere - so if `Logger` satisfied some interface, `Server` now satisfies it too. Exported fields of an embedded struct are promoted the same way, reachable as `s.Field`. Promotion is name resolution, not dynamic dispatch: nothing about it makes `Server` a kind of `Logger`, and a `*Server` cannot be passed where a `*Logger` is wanted. ### 2. A named field ```go type Server struct { logger Logger } ``` Now nothing is promoted. Code inside the package reaches the dependency as `s.logger.Log(...)`, and if the outside world should be able to log through a `Server`, you write that method yourself: ```go func (s *Server) Log(msg string) { s.logger.Log(msg) } ``` That is more typing, and it is the whole point: every method on your type is one you chose, named and documented. You can also swap the field's type, wrap it, guard it, or add a parameter, without anything leaking that you did not intend. ### 3. An interface The third form removes the concrete dependency altogether. Declare the field or parameter as an interface type and accept anything with the right methods. Because satisfaction is implicit, the implementing type does not need to know your interface exists - it can live in a package that was written years earlier. This is what makes small interfaces so cheap in Go, and it is the mechanism behind substituting a fake in a test or a different backend in production. ### Choosing between them A useful rule of thumb: - **Embed** when the embedded type's *entire* API is deliberately part of what your type offers - for example when you are decorating something and want every method you did not override to pass through unchanged. Embedding is the only one of the three that gives you that for free. - **Named field** by default, especially on an exported struct. You pay a few forwarding methods and you keep control of your surface. - **Interface** when you want the dependency to be replaceable, or when your type genuinely does not care which implementation it gets. ### Consequences people are surprised by Embedding reaches further than the method set: - **Encoding.** `encoding/json` treats an embedded struct's exported fields as if they were declared on the outer struct, so they appear at the top level of the JSON object rather than nested. Embedding therefore changes your wire format, not only your call sites. Giving the embedded field a JSON tag turns it back into a normal nested object. - **Interface satisfaction.** Gaining a method silently can change behaviour at a distance. A type that embeds something with a `String() string` method becomes a `fmt.Stringer`, and `fmt.Println` will print it differently from the day before. - **Visibility.** Whether a promoted method is exported depends on the *method's* name, not the field's. An unexported embedded type still promotes its exported methods onto an exported outer type. - **Name collisions.** The embedded type's names live in your struct's namespace, so a second embedded field with a matching name creates an ambiguity that the compiler reports where the selector is used. The short version: a named field costs you keystrokes, an embedded field costs you control. Both are composition; only one of them is also publication.

  • Does embedding make the outer type satisfy interfaces the embedded type satisfied?
    Yes. Promoted methods are in the outer type's method set, so if `Logger` satisfied an interface with a `Log` method, `Server` satisfies it too, with no declaration anywhere. That is often exactly why you embed - and also why an accidental embed silently widens what your type can be passed to.
  • What does embedding do to the struct's JSON encoding?
    `encoding/json` promotes an embedded struct's exported fields into the outer JSON object, so they appear at the top level instead of nested under a field name. Adding a JSON tag to the embedded field makes it a normal nested object again. So embedding changes your serialised shape as well as your method set.
  • If you only want two of the embedded type's ten methods, what should you write?
    A named field plus two forwarding methods. Embedding is all-or-nothing: you cannot promote a subset, and you cannot un-promote one later without breaking callers. Two three-line methods buy you a surface you chose deliberately.

Embedding is like putting a colleague's desk phone on your own desk under your name: every call they could take, callers now place with you. A named field is keeping their number in a drawer and passing on only the calls you choose.

saying these in an interview costs you the question

  • Calls embedding inheritance and talks about subclasses
  • Thinks an embedded field's methods stay private to the outer package
  • Believes a type must declare that it implements an interface
  • Assumes embedding copies fields but not methods
  • Expects to promote only some of the embedded type's methods