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.

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:
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())
}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:
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))
}# example.test/lab
./main.go:17:23: cannot use d (variable of struct type Dog) as Animal value in argument to describeThe 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:
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())
}# example.test/lab
./main.go:21:16: ambiguous selector d.MoveThe 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:
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)
}hello, world
second line
25countingWriter 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:
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))
}{"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.


