Skip to content
gohacks.dev
Basics

Embedding in Go Is Not Inheritance and Here Is Why

Embedding promotes fields and methods, but the inner type never sees the outer one. See how that differs from inheritance, plus name clashes and JSON.

7 min read
Summarize in AI
Flat illustration of an editor window full of code.

Almost everyone who writes object-oriented code arrives at embedding and reads it as extends. The syntax invites it: write Animal inside Dog and Dog can call everything Animal can, plus its own. The resemblance ends the moment you ask a promoted method to call something the outer type overrides. Embedding in Go is a shorthand for composition, the compiler writes the forwarding calls, and everything you know about how Go types work, from structs to interfaces, still holds. This article runs the cases that show where the inheritance model breaks, and the two places where embedding really earns its keep. It belongs to the Go basics path.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

What is promoted

Declare a field with only a type name and Go calls it an embedded field. Its name is the type's name, and the exported fields and methods of that type become reachable on the outer value without the extra step:

main.goGo
package main

import "fmt"

type Animal struct {
	Name string
}

func (a Animal) Speak() string { return "..." }

func (a Animal) Intro() string { return a.Name + " says " + a.Speak() }

type Dog struct {
	Animal
	Breed string
}

func (d Dog) Speak() string { return "woof" }

func main() {
	d := Dog{Animal: Animal{Name: "Rex"}, Breed: "lab"}

	fmt.Println(d.Name, d.Animal.Name)
	fmt.Println(d.Speak())
	fmt.Println(d.Animal.Speak())
	fmt.Println(d.Intro())
}
Text
Rex Rex
woof
...
Rex says ...

d.Name is the same as d.Animal.Name. Dog defines its own Speak, which wins over the promoted one for d.Speak(), and the inner version is still reachable through the explicit path. All of that matches an inheritance model. The last line does not. Intro was promoted from Animal, and inside it the call a.Speak() runs on an Animal. It prints Rex says ..., not Rex says woof. The Animal value inside the dog has no idea it lives in a dog, so there is no override for it to find. Languages with virtual methods would print woof here, and this is the single clearest difference.

If you want the outer behavior inside shared code, you must pass it in: a method that takes an interface, a function field, or the outer type's own Intro that calls d.Speak(). Go does not do it implicitly, by design, and the benefit is that the code that runs is the code you can read in one type.

An outer type is not an inner one

Inheritance gives you substitutability: a dog is an animal, so a dog fits where an animal is expected. Embedding gives you nothing like that:

main.goGo
package main

import "fmt"

type Animal struct{ Name string }

type Dog struct {
	Animal
	Breed string
}

func describe(a Animal) string { return a.Name }

func main() {
	d := Dog{Animal{"Rex"}, "lab"}
	fmt.Println(describe(d.Animal))
	fmt.Println(describe(d))
}
Text
# example.test/lab
./main.go:17:23: cannot use d (variable of struct type Dog) as Animal value in argument to describe

The first call passes the embedded field explicitly and compiles. The second is rejected: a Dog is a different type. The substitutability you want in Go comes from interfaces. If both types have Speak() string, both satisfy a Speaker interface, and a function that takes the interface accepts either. Promoted methods count toward satisfying interfaces, which is the main practical use of embedding, but the thing you pass around is the interface, not the base struct. Write small interfaces where they are used, as the interfaces guide recommends, and do not model a class tree.

Name collisions

Embedding two types that have a field or method with the same name is legal, as long as you never use the ambiguous name unqualified:

main.goGo
package main

import "fmt"

type Walker struct{}

func (Walker) Move() string { return "walk" }

type Swimmer struct{}

func (Swimmer) Move() string { return "swim" }

type Duck struct {
	Walker
	Swimmer
}

func main() {
	var d Duck
	fmt.Println(d.Walker.Move(), d.Swimmer.Move())
	fmt.Println(d.Move())
}
Text
# example.test/lab
./main.go:21:16: ambiguous selector d.Move

The explicit calls are fine. The bare d.Move() fails with ambiguous selector d.Move. The rule is based on depth: the shallowest match wins, and two matches at the same depth are an error, but only if you try to use them. A shadowing method defined on the outer type, as Dog.Speak earlier, sits at depth zero and beats anything embedded. This lookup is also why adding a method to an embedded type in a library can break your code when it collides with a method you embedded from somewhere else, a risk worth weighing before you embed a type you do not control.

Pointer receivers and method sets

Embedding a value T promotes its value-receiver methods to both Outer and *Outer, and its pointer-receiver methods to *Outer only. Embedding *T promotes both sets to both. That follows from the method set rules, and it decides whether your outer type satisfies an interface. If a type seems not to satisfy an interface after embedding, check whether the missing method has a pointer receiver and whether you are using the outer type by value. Pointer or value receiver covers the underlying rules.

Embedding an interface to override one method

The best use of embedding is not code reuse. It is to wrap an interface and change one method while delegating the rest. Here is a writer that counts the bytes it passes through:

main.goGo
package main

import (
	"bytes"
	"fmt"
	"io"
)

type countingWriter struct {
	io.Writer
	n int
}

func (w *countingWriter) Write(p []byte) (int, error) {
	w.n += len(p)
	return w.Writer.Write(p)
}

func main() {
	var buf bytes.Buffer
	cw := &countingWriter{Writer: &buf}

	fmt.Fprintf(cw, "hello, %s\n", "world")
	fmt.Fprintln(cw, "second line")

	fmt.Print(buf.String())
	fmt.Println(cw.n)
}
Text
hello, world
second line
25

countingWriter satisfies io.Writer through its own Write, and the embedded io.Writer field holds the real destination. Had io.Writer had ten methods, the wrapper would still have all ten without typing them, which is how the standard library builds wrappers like sort.Reverse, http.ResponseWriter decorators and test doubles that override only the call under test. It is the same idea as a decorator in other languages, and it needs three lines of setup. A caveat applies: if the embedded interface field is nil, calling a promoted method panics with a nil pointer dereference, so make sure it is set.

Embedding and JSON

The other place embedding changes behavior is serialization. encoding/json treats the exported fields of an embedded struct as if they were declared on the outer struct:

main.goGo
package main

import (
	"encoding/json"
	"fmt"
)

type Meta struct {
	ID      int    `json:"id"`
	Created string `json:"created"`
}

type Post struct {
	Meta
	Title string `json:"title"`
}

type Named struct {
	Meta  Meta   `json:"meta"`
	Title string `json:"title"`
}

func main() {
	flat, _ := json.Marshal(Post{Meta{7, "monday"}, "hello"})
	nested, _ := json.Marshal(Named{Meta{7, "monday"}, "hello"})
	fmt.Println(string(flat))
	fmt.Println(string(nested))
}
Text
{"id":7,"created":"monday","title":"hello"}
{"meta":{"id":7,"created":"monday"},"title":"hello"}

The embedded Meta is flattened into the top level, and the same data in a named field nests under meta. Flattening is convenient for shared fields like IDs and timestamps. It is also how a field in the embedded struct silently collides with a field of the same JSON name in the outer one, in which case the shallower one wins and the other disappears from the output. Struct tags and what reads them covers the options that control this.

When to embed, and when to name the field

Embed when the outer type really is the inner type plus a bit more and you want the whole method set promoted. Wrappers and decorators fit. Value types with a shared header of fields fit moderately well. Do not embed to save typing a field name. Embedding exports the inner type's whole method set through your type, so type Counter struct { sync.Mutex; n int } makes Lock and Unlock public methods of Counter, and any caller can lock your internals. Prefer a named, unexported field, mu sync.Mutex, in that case. And if you only need one function of the inner type, call it explicitly. The specification on struct types defines promotion precisely, and Effective Go on embedding shows the interface-embedding idiom with bufio.ReadWriter.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.