Before you begin
Why this matters
Embedding is common in Go code and in the standard library, and most of its surprises come from one picture: that the outer struct has the inner type’s fields and methods as its own. If you can follow a promoted name to the value it reaches, you can predict which value a method changes, which interfaces a type satisfies, and when a call panics.
What this helps you reason about
- Say which field a name such as m.Name reaches, and read the error ambiguous selector.
- Predict which receiver a promoted method gets, and whether the outer struct or a pointer to it satisfies an interface.
- Spot a nil embedded pointer, an embedded interface with missing methods, and a promoted method that a library calls.
In Java or Python, a class that extends another class is a kind of that class, and the parent’s methods can call the child’s versions. Go has no classes and no inheritance. It has embedding: a struct holds a value of another type in a field without a name, and the outer struct can use that value’s fields and methods directly. It looks like inheritance in a few lines of code. This lesson follows each promoted name to the value it really reaches, so you can predict what embedding does in the cases where it is not inheritance.
Already know this? The short version
- An embedded field is a field named after its type; m.Level is shorthand for m.Player.Level, and there is one Level, inside m.Player. Since Go 1.27 a struct literal may set a promoted field directly, but not through an embedded pointer.
- The shallowest name wins: an outer field or method hides a promoted one. Two promoted names at the same depth give ambiguous selector m.Name, reported only where the name is used.
- m.AddWin() with a pointer receiver becomes (&m.Player).AddWin(). Embedding T gives S the value-receiver methods of T and gives *S all of them; embedding *T gives both S and *S all of them.
- An embedded *T is shared by copies of the outer struct and is nil until set; an embedded interface makes the struct satisfy it at once, and a call to a method nobody wrote panics. “What goes wrong with embedding in real programs?” covers nil embedded pointers, fakes that embed an interface, an embedded sync.Mutex, and JSON.
Words in this lesson (7)
- embedding
- Writing a type inside a struct without a field name, as Claim in struct { Claim; Nights int }. The field is named after its type, and its fields and methods are promoted.
- promoted field
- A field of an embedded field that can be used through the outer struct, as m.Level for m.Player.Level. It is a shorter path to the one field inside the embedded value, not a second copy.
- promoted method
- A method of an embedded field that can be called on the outer struct, as h.Line() for h.Claim.Line(). It still runs with the embedded value as its receiver.
- ambiguous selector
- A name such as m.Name that two embedded fields provide at the same depth. The compiler rejects it where it is used; the full path, such as m.Contact.Name, still works.
- pointer receiver
- A receiver declared as a pointer, as in func (w *Ward) Admit(). The method gets the address of the value it is called on, so it can change that value.
- method set
- The methods a type has for satisfying interfaces. A struct type such as Scooter has its value-receiver methods; the pointer type *Scooter has both its value-receiver and its pointer-receiver methods.
- panic
- Go’s way of stopping a program because of a mistake in the code. panic("message") stops the current function and every function that called it, running their deferred calls on the way. Unless a deferred call recovers, the program prints panic: message and a stack trace, then exits.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Embedded fields
What does embedding put inside a struct?
Dispatch, inheritance, and composition showed that embedding promotes the inner type’s fields and methods, that a promoted method runs on the inner value, and that the inner type never calls the outer type’s methods. This lesson looks at the embedded field itself.
An embedded field is a field written as a type without a name. Go names the field after its type, so in Member below the field is called Player and holds a whole Player value. You set it like any other field: Member{Player: Player{Name: "Ines"}, Fee: 30}.
A promoted field is a shorter path to a field inside it. m.Level means m.Player.Level; the compiler writes the long path for you. Nothing is copied up into Member. There is one Level, inside m.Player, so m.Level and m.Player.Level can never differ. Promoted methods work the same way: m.Badge() means m.Player.Badge().
type Member struct {
Player // a field named Player, of type Player
Fee int
}
m := Member{Player: Player{Name: "Ines", Level: 2}, Fee: 30}
m.Level = 3 // the same as m.Player.Level = 3 An embedded field is a field named after its type; a promoted name is a shorter path through that field.
Coming from Java: A Member that embeds Player is not a Player: there is no super, no protected, and no cast between them. The Player is a value stored in a field, and m.Player names it.
Precisely: what can be embedded, and literals
The specification calls this an embedded field and allows a type name T or a pointer *T, where T is not itself a pointer type; T may be an interface. The field name is the type name without any package name, so embedding sync.Mutex gives a field named Mutex. Since Go 1.27, a struct literal may also set a promoted field directly, Member{Name: "Ines", Fee: 30}, which earlier versions reject. It still cannot reach through an embedded pointer.
Experiment
Write a promoted field and find where it went
The embedded field is named Player, after its type, and holds a whole Player. m.Level is shorthand: the compiler writes m.Player.Level in its place. There is one Level, inside m.Player, so the two names always agree. A named field is an ordinary field, and nothing is promoted from it.
Why it matters A promoted field is a path, not a second field. When you read m.Level, picture m.Player.Level.
Predict both lines, and whether m.Level and m.Player.Level can ever differ. Then change the line Player inside Member to Player Player, which gives the field a name, and predict again.
package main
import "fmt"
type Player struct {
Name string
Level int
}
func (p Player) Badge() string {
return fmt.Sprint(p.Name, " (level ", p.Level, ")")
}
type Member struct {
Player
Fee int
}
func main() {
m := Member{Player: Player{Name: "Ines", Level: 2}, Fee: 30}
m.Level = 3
fmt.Println(m.Badge())
fmt.Println(m.Player.Level, m.Fee)
}
What to look for
Before line 20 runs, m can show leftover values; Go has not set it yet. After line 20, m shows main.Member {Player: {Name: "Ines", Level: 2}, Fee: 30}: the Player is one field of m, with its own fields inside. After m.Level = 3, the 3 appears inside Player. Member has no Level of its own anywhere. Step into m.Badge(): the receiver p shows main.Player {Name: "Ines", Level: 3}. The run prints Ines (level 3), then 3 30. After the edit, the program does not compile: ./main.go:21:7: m.Level undefined (type Member has no field or method Level), and the same for m.Badge on line 22.
Expected output
Ines (level 3)
3 3002 / Which name wins
Which field does a name reach when several have it?
Several fields can share a name: the outer struct may declare its own Name, and two embedded types may both have one. The compiler then looks for the name in steps. First it looks in the outer struct itself. If the name is not there, it looks in the fields embedded in it, then in the fields embedded in those. It stops at the first step that finds the name. The specification calls each step a depth.
So an outer Name hides Player.Name and Contact.Name. The hidden fields are still there, and the full paths m.Player.Name and m.Contact.Name reach them. The same rule applies to methods, as an outer Line hid Claim.Line in the earlier lesson.
When two names are found at the same depth, the compiler refuses to choose. The error appears only where the name is used, so a struct with two embedded Name fields compiles until some code writes m.Name. The compiler reports ambiguous selector m.Name, with the line of that use. Write the full path to choose one.
type Member struct {
Player // has Name
Contact // has Name and Phone
}
m.Phone // fine: only Contact has Phone
m.Contact.Name // fine: the full path
m.Name // ambiguous selector m.Name The shallowest name wins; two names at the same depth are an error where the name is used.
Coming from Python: With two base classes that both define name, Python picks the first base in the class statement. Go never picks: two embedded names at the same depth are a compile error where they are used.
Precisely: depth and hiding
The specification says that a selector x.f denotes the field or method at the shallowest depth where an f exists, and that it is illegal if there is not exactly one f at that depth. A field and a method with the same name at the same depth also conflict. The specification uses the word shadow for an outer name that hides a deeper one. This is not the shadowing of a variable by an inner block.
Experiment
Find which Name a selector reaches
The compiler looks for Name in Member itself first, then in the fields embedded in Member, then one level deeper. It stops at the first level where it finds the name. With the outer Name deleted, it finds two at the same level and refuses to choose.
Why it matters An outer name hides a promoted one. Two promoted names at the same level are an error where you use them; write the full path, m.Contact.Name.
Member has its own Name, and both embedded types have one too. Predict the first line. Then delete the line Name string from Member and the line Name: "Ines D.", from main, and predict again.
package main
import "fmt"
type Player struct{ Name string }
type Contact struct {
Name string
Phone string
}
type Member struct {
Name string
Player
Contact
}
func main() {
m := Member{
Name: "Ines D.",
Player: Player{Name: "Ines"},
Contact: Contact{Name: "Rui", Phone: "555 0101"},
}
fmt.Println(m.Name, "/", m.Phone)
fmt.Println(m.Player.Name, "/", m.Contact.Name)
}
What to look for
After the literal runs, m shows three names: Name: "Ines D." at the top, "Ines" inside Player, and "Rui" inside Contact. The first Println reads the top one and the Phone inside Contact, and prints Ines D. / 555 0101. The second line names each path in full and prints Ines / Rui. After the edit, the program does not compile: ./main.go:22:19: ambiguous selector m.Name. m.Phone still compiles, because only Contact has a Phone.
Expected output
Ines D. / 555 0101
Ines / Rui03 / Receivers
Which receiver does a promoted method get?
A promoted method is a method of the inner type, so it gets the inner value as its receiver. With a value receiver, it gets a copy of m.Player. With a pointer receiver, the compiler passes the address of the field inside m: m.AddWin() becomes (&m.Player).AddWin(), and AddWin changes m.Player in place.
Interfaces need more care. Interfaces, traits, and type classes showed that the method set of Player holds its value-receiver methods, and the method set of *Player holds both kinds. Embedding passes these on as follows:
- A struct S that embeds T gets the value-receiver methods of T. *S gets all methods of T.
- A struct S that embeds *T gets all methods of T, and so does *S.
So with Player embedded, &m satisfies an interface that needs AddWin, and m does not. The compiler reports Member does not implement Winner (method AddWin has pointer receiver). With *Player embedded, m satisfies it too, because the copy of m inside the interface holds the same Player pointer.
func (p *Player) AddWin() { p.Wins++ }
type Member struct {
Fee int
Player
}
m.AddWin() // (&m.Player).AddWin()
var w Winner = &m // fine: *Member has AddWin
var v Winner = m // error: Member does not have it A promoted method gets the inner value or its address; embedding T gives S only T’s value-receiver methods.
Precisely: the wrapper methods
For each promoted method, the compiler writes a short method of the outer type that calls the inner one with the right receiver. A call through an interface goes through that method; the stepper does not show a frame for it. A direct call such as m.AddWin() calls (*Player).AddWin directly, with &m.Player.
Experiment
Follow a promoted pointer method into the inner struct
m.AddWin() means m.Player.AddWin(). AddWin needs a *Player, and m is a variable, so the compiler passes &m.Player, the address of the field inside m. An interface value cannot do that for you: Member’s method set holds only Player’s value-receiver methods, and *Member’s holds both kinds.
Why it matters A promoted pointer method changes the inner struct of the value you called it on. To use the outer struct as an interface that needs such a method, store a pointer to it.
AddWin has a pointer receiver. Predict what is printed after two wins, one through m and one through an interface. Then change var w Winner = &m to var w Winner = m and predict again.
package main
import "fmt"
type Player struct {
Name string
Wins int
}
func (p *Player) AddWin() {
p.Wins++
}
type Member struct {
Fee int
Player
}
type Winner interface{ AddWin() }
func main() {
m := Member{Fee: 30, Player: Player{Name: "Ines"}}
m.AddWin()
var w Winner = &m
w.AddWin()
fmt.Println(m.Name, m.Wins)
}
What to look for
Step into m.AddWin(). The frame is main.(*Player).AddWin, and p shows an address, → and main.Player {Name: "Ines", Wins: 0}: a pointer to the Player inside m, not to m. After p.Wins++, select main’s frame: m shows Wins: 1 inside Player. After var w Winner = &m, w shows main.Winner(*main.Member) and an address 8 bytes lower than p: m starts with Fee, and its Player comes after it. Step into w.AddWin(): the stepper stops in main.(*Player).AddWin again, directly above main, and p holds the same address as before. The run prints Ines 2. After the edit, the program does not compile: ./main.go:24:20: cannot use m (variable of struct type Member) as Winner value in variable declaration: Member does not implement Winner (method AddWin has pointer receiver).
Expected output
Ines 204 / Embedded pointers
What changes when you embed a pointer?
A struct can embed a pointer type, such as *Player. The field is still named Player, but it holds an address. Promoted names read through that address: b.Wins means (*b.Player).Wins.
This changes what a copy does. Copying a Member copies the pointer, so every copy reaches the same Player, while ordinary fields such as Fee are copied. Records, structs, and objects showed the same for a pointer in a named field; with embedding, the shared field hides behind short names like b.Wins.
The zero value of the field is nil. A promoted name then has no Player to reach, and Go panics with runtime error: invalid memory address or nil pointer dereference. With a pointer receiver, the call b.AddWin() still starts, with p set to nil, and the panic comes at the first line that reads a field through p.
type Member struct {
*Player // a field named Player, of type *Player
Fee int
}
a := Member{Player: &Player{Name: "Ines"}, Fee: 30}
b := a // b.Player holds the same address
b.AddWin() // a.Wins is now 1 too Copies of a struct that embeds a pointer share the inner value; a nil embedded pointer panics at the first promoted field read.
Coming from C: A C struct can contain another struct as a member, but member access never reaches through a pointer for you. In Go, b.Wins follows the embedded pointer, so a short name can hide a nil dereference.
Experiment
Copy a struct that embeds a pointer
The embedded field is a *Player named Player. Copying a Member copies that pointer, so both copies reach one Player, while Fee is copied. A promoted name reads through the pointer: b.Wins means (*b.Player).Wins. When the pointer is nil, there is no Player to read.
Why it matters An embedded pointer is shared by every copy of the outer struct, and its zero value is nil. Set it before using any promoted name.
b := a copies a Member. Predict both lines: which numbers does the change to b reach? Then change the line a := Member{…} to a := Member{Fee: 30}, so no Player is set, and predict again.
package main
import "fmt"
type Player struct {
Name string
Wins int
}
func (p *Player) AddWin() {
p.Wins++
}
type Member struct {
*Player
Fee int
}
func main() {
a := Member{Player: &Player{Name: "Ines"}, Fee: 30}
b := a
b.Fee = 0
b.AddWin()
fmt.Println(a.Wins, a.Fee)
fmt.Println(b.Wins, b.Fee)
}
What to look for
After line 20, a shows main.Member {Player: 0x… → {Name: "Ines", Wins: 0}, Fee: 30}: the field holds an address, and after → the Player it points to. After b := a, b shows the same address. b.Fee = 0 changes b only. Step into b.AddWin(): p holds that same address. Back in main, a and b both show Wins: 1. The run prints 1 30, then 1 0. After the edit, a shows Player: nil. The call b.AddWin() still enters AddWin, with p showing *main.Player nil, and the run stops at p.Wins++ with runtime error: invalid memory address or nil pointer dereference. The stepper shows a panic’s message without the word panic. At the last stop, p can show unreadable: the debugger reads it after the failure.
Expected output
1 30
1 005 / Embedded interfaces
What does embedding an interface in a struct give you?
An interface type can be embedded in a struct too. The field is named after the interface and can hold any value that satisfies it; its zero value is nil. All the interface’s methods are promoted, so the struct satisfies the interface at once, even with nothing stored in the field.
This is used in two ways. A wrapper stores a real value in the field and writes one method of its own, which hides the promoted one; every other call goes to the stored value. A test fake writes only the methods the test needs and leaves the field nil.
The cost is that the compiler cannot tell you that a method is missing. Without the embedded Board, passing a testBoard would report *testBoard does not implement Board (missing method Clear). With it, the program compiles, and a call to Clear calls Clear on a nil interface, which panics with invalid memory address or nil pointer dereference.
type testBoard struct {
Board // nil: makes testBoard satisfy Board
lines []string
}
func (t *testBoard) Show(line string) {
t.lines = append(t.lines, line)
} An embedded interface satisfies the interface at once; a promoted method with no value behind it panics when called.
Coming from Java: A Java class that implements an interface must write every method or be declared abstract. A Go struct that embeds the interface compiles with none of them, and a missing method fails only when it is called.
Experiment
Call a method that an embedded interface does not have
Embedding Board promotes both of its methods, so *testBoard has Show and Clear and satisfies Board. testBoard’s own Show hides the promoted one. Clear is still the promoted method: it calls Clear on the Board stored in the field, and the field is nil.
Why it matters Embedding an interface satisfies it at once, whether or not a value is stored in the field. The compiler no longer tells you which methods are missing; the call does.
testBoard writes only Show, yet it is passed as a Board. Predict what is printed. Then add the line b.Clear() as the first line of announce, and predict again.
package main
import "fmt"
type Board interface {
Show(line string)
Clear()
}
// testBoard records lines instead of lighting a screen.
type testBoard struct {
Board
lines []string
}
func (t *testBoard) Show(line string) {
t.lines = append(t.lines, line)
}
func announce(b Board, name string) {
b.Show("next: " + name)
}
func main() {
t := &testBoard{}
announce(t, "Ines")
fmt.Println(t.lines)
}
What to look for
After the first line of main, t shows an address, then → and the testBoard it points to. Its embedded field is named Board, and it shows nil: nothing is stored in it. Step into announce and then into b.Show: the frame is main.(*testBoard).Show, testBoard’s own method. The run prints [next: Ines]. After the edit, the run stops before Show is ever called, with runtime error: invalid memory address or nil pointer dereference, and nothing is printed. The last stop shows the line announce(t, "Ines") in main with unreadable values: the failure is in the Clear method that the compiler wrote, which has no line of your own.
Expected output
[next: Ines]06 / In production
What goes wrong with embedding in real programs?
Four embedding bugs reach real programs.
A nil embedded pointer. type Club struct { *log.Logger; Name string } looks ready to use, but in var c Club the Logger is nil, and c.Println("open") panics inside the log package. Give such a type a constructor function that sets the pointer, or embed a value when the zero value must work.
A fake that embeds an interface. It keeps compiling when the interface gains a method. When the code under test starts to call that method, the test panics instead of failing to compile.
A copied lock. A sync.Mutex is a lock that lets one goroutine (a function running at the same time as others) into a piece of code at a time. Embedding it promotes Lock and Unlock into the outer type’s methods. Copying the struct copies the lock with its state, so a method with a value receiver locks its own copy and protects nothing. go vet reports this, for example Points passes lock by value. Use pointer receivers, and prefer a named field such as mu sync.Mutex, so Lock does not become part of the outer type.
JSON promotion. encoding/json, Go’s package for JSON text, writes an embedded struct’s fields as if they were the outer struct’s own. Two of them with the same JSON name at the same depth are both left out, with no error. A promoted MarshalJSON, the method json uses to write a type, takes over the whole value: a struct that embeds time.Time is written as one time string. The experiment shows this.
type Result struct {
time.Time // promotes MarshalJSON
Winner string
}
played := time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC)
out, _ := json.Marshal(Result{Time: played, Winner: "Ines"})
// out holds only "2026-10-03T00:00:00Z" Check that an embedded pointer is set, that a fake has every method that is called, that a lock is never copied, and which methods a library will find.
Experiment
Turn a match result into JSON
MarshalJSON is promoted from Day, so Result has a MarshalJSON method too. json.Marshal checks for that method first and lets it write the whole value. The promoted method gets only r.Day, so it cannot write Winner or Score. With a named field, Result has no MarshalJSON, and json writes each field by name, using Day’s method for that one field only.
Why it matters Before you embed a type, check its methods: every one of them becomes a method of the outer struct, including ones that a library looks for.
Day has its own MarshalJSON method, and Result embeds Day. Predict the printed JSON: which fields appear? Then change the line Day inside Result to Day Day, which gives the field a name, and predict again.
package main
import (
"encoding/json"
"fmt"
)
type Day struct{ Date string }
func (d Day) MarshalJSON() ([]byte, error) {
return json.Marshal("played " + d.Date)
}
type Result struct {
Day
Winner string
Score string
}
func main() {
r := Result{Day: Day{Date: "3 Oct"}, Winner: "Ines", Score: "21-17"}
out, err := json.Marshal(r)
fmt.Println(string(out), err)
}
What to look for
Step into json.Marshal(r): the next stop is in Day.MarshalJSON, directly above main, because the stepper does not show the frames of encoding/json in between. Its receiver d shows main.Day {Date: "3 Oct"}: only the Day, not the whole Result. The run prints "played 3 Oct" <nil>: one JSON string, with no Winner and no Score. Back in main, out shows bytes as numbers ([34, 112, …]); string(out) turns them into text. After the edit, the stepper still enters Day.MarshalJSON, now for the field Day only, and the run prints {"Day":"played 3 Oct","Winner":"Ines","Score":"21-17"} <nil>.
Expected output
"played 3 Oct" <nil>07 / Try it
Can you predict which games are counted?
Two pairs share one Stats. Pair has its own Show, and Stats has one too.
package main
import "fmt"
type Stats struct{ Played int }
func (s *Stats) Play() { s.Played++ }
func (s Stats) Show() string {
return fmt.Sprint(s.Played, " games")
}
type Pair struct {
Name string
*Stats
}
func (p Pair) Show() string {
return p.Name + ": " + p.Stats.Show()
}
func main() {
a := Pair{Name: "Ana and Bo", Stats: &Stats{}}
b := a
b.Name = "Cy and Di"
a.Play()
b.Play()
fmt.Println(a.Show())
fmt.Println(b.Stats.Show())
}
What are the two output lines?
Reveal the reasoning
Ana and Bo: 2 games, then 2 games. b := a copies Name and the *Stats pointer, so both pairs reach one Stats, and the two Play calls count 2 games in it. Changing b.Name does not change a. a.Show() runs Pair’s own Show, because the outer method hides the promoted one; it calls Stats’s Show through the full path p.Stats.Show(). b.Stats.Show() reaches Stats’s Show directly, so it prints the count without a name.
Practice tickets
- GO-501 · Members pay the full price for a surfboard → Free
- GO-502 · A coach is booked for more hours than the weekly limit → Sign in · Pro
- GO-503 · The team card desk stops at a team without a sponsor → Sign in · Pro
Keep exploring
Go error values, wrapping, and errors.Is →