Go Module 5 – Structs, pointers and methods
Embedding: composition instead of inheritance
Embed Go structs and interfaces to reuse fields and methods, see how promotion and shadowing pick a name, and why promoted methods never call the outer type's.
What you will learn
- Use an embedded struct and its promoted fields and methods
- Predict which field or method a selector picks, and when it is ambiguous
- Explain why a promoted method never calls the outer type's methods
- Combine types by embedding interfaces in structs and in interfaces
- Rewrite a class hierarchy as parts and an interface
Before you start
What you’ll learn · Before you start
What you will learn
- Use an embedded struct and its promoted fields and methods
- Predict which field or method a selector picks, and when it is ambiguous
- Explain why a promoted method never calls the outer type's methods
- Combine types by embedding interfaces in structs and in interfaces
- Rewrite a class hierarchy as parts and an interface
Before you start
On this page
A Service that embeds a Logger can call svc.Log("starting") without declaring a single method, and a Java
developer reading it sees a subclass. It is not one: when a method of the Logger calls another method, it calls the
Logger’s own version, even if Service declares one with the same name.
You will embed a struct and use what it promotes, predict which name a selector finds, and see why nothing is ever overridden. Under each program is the output Go 1.27.1 printed when it ran.
An embedded field lends the outer type its fields and methods
type Service struct { Logger; Name string } has two fields. Logger is an
embedded field: A struct field declared with a type and no name, such as Audit in struct { Audit; ID string }. The type's name serves as the field's name, and its fields and methods are promoted to the outer struct.: declared with a type and no name, it is called after its type, and
its fields and methods are promoted field: A field or method of an embedded field that can be used as if the outer struct declared it, such as o.CreatedBy for o.Audit.CreatedBy, unless a name at a shallower depth hides it., usable as if Service had declared them:
// service.go: a Service embeds a Logger and gains its field and its method.
package main
import "fmt"
type Logger struct {
Prefix string
}
func (l *Logger) Log(msg string) {
fmt.Println(l.Prefix, msg)
}
type Service struct {
Logger // embedded: Prefix and Log are promoted to Service
Name string
}
// LogWriter is any type that has a Log method.
type LogWriter interface {
Log(msg string)
}
func main() {
svc := &Service{Logger: Logger{Prefix: "[pay]"}, Name: "payments"}
svc.Log("starting " + svc.Name) // promoted: svc.Logger.Log
svc.Prefix = "[pay v2]" // a promoted field
svc.Logger.Log("the long way round")
var w LogWriter = svc // *Service has Log, through its embedded Logger
w.Log("through an interface")
fmt.Printf("%+v\n", *svc)
} Output
[pay] starting payments
[pay v2] the long way round
[pay v2] through an interface
{Logger:{Prefix:[pay v2]} Name:payments}
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run service.go
svc.Log and svc.Prefix worked although Service declares neither, and svc.Logger.Log reached the same method by
its full name. Step through how Go finds them:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Depth 0. For svc.Log, Go first looks at what Service declares itself, the field Name and the embedded field Logger. Neither is called Log.
- Depth 1. One level down, inside the embedded Logger, it finds Log, a method of *Logger. So svc.Log is short for svc.Logger.Log.
- The receiver is the inner value. The call runs Log with the receiver &svc.Logger, not svc. Log sees a Logger and its Prefix, and knows nothing about Service.
- A promoted field. svc.Prefix is found the same way, at depth 1, so svc.Prefix = "[pay v2]" writes the Prefix inside the embedded Logger.
- It counts for interfaces. Because *Service has Log through Logger, a *Service can be stored in a LogWriter variable, and w.Log runs Logger's method.
Go searched by depth: first what Service declares itself, then what its embedded Logger holds, one level down. The
method ran with the inner value as its receiver, &svc.Logger, so Log never saw the Service around it. A promoted
method also counts when Go checks an
A type that lists method signatures, such as LogWriter with Log(msg string). Any type whose methods include them can be stored in a variable of that interface type, with nothing to declare.:
*Service has Log through Logger, so a *Service could be stored in a LogWriter. An embedded field can also be an
interface, whose methods call the value it holds, and an interface can embed others, as io.ReadWriter embeds
io.Reader and io.Writer.
Checkpoint
Check: Embedded fields and promotion
The shallowest name wins, and a tie is an error only when used
When the outer type declares a name that an embedded field also has, the outer one is found first, at depth 0, and hides the other. Two embedded fields at the same depth may both have a name:
// shadow.go: the outer type's own method wins over a promoted one, and both embedded fields stay reachable.
package main
import "fmt"
type Logger struct{ Prefix string }
func (l Logger) Log(msg string) { fmt.Println(l.Prefix + msg) }
type Metrics struct{ Prefix string }
func (m Metrics) Count(name string) { fmt.Println(m.Prefix + "count " + name) }
type Service struct {
Logger
Metrics
}
// Log shadows Logger.Log, and still calls it by its full name.
func (s Service) Log(msg string) {
s.Logger.Log("service: " + msg)
}
func main() {
s := Service{Logger{"[log] "}, Metrics{"[metrics] "}}
s.Log("ready") // Service.Log, at depth 0
s.Count("requests") // Metrics.Count, at depth 1
fmt.Printf("%q %q\n", s.Logger.Prefix, s.Metrics.Prefix)
} Output
[log] service: ready [metrics] count requests "[log] " "[metrics] "
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run shadow.go
s.Log ran Service’s own Log, which called the hidden s.Logger.Log by its full name. s.Count came from Metrics.
Logger and Metrics both have a Prefix, and that compiled because no line used s.Prefix; the program named each in
full. Asking for the shared name is what the specification forbids:
// ambiguous.go: two embedded fields at the same depth both have a Prefix.
package main
import "fmt"
type Logger struct{ Prefix string }
type Metrics struct{ Prefix string }
type Service struct {
Logger
Metrics
}
func main() {
s := Service{Logger{"[log] "}, Metrics{"[metrics] "}}
fmt.Println(s.Prefix)
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments ./ambiguous.go:16:16: ambiguous selector s.Prefix
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run ambiguous.go
ambiguous selector s.Prefix: there is not exactly one Prefix at the shallowest depth, so the compiler refuses to
guess. A duplicate name at one level is harmless as long as nothing asks for it by its short name.
Checkpoint
Check: Shadowing and ambiguous names
A promoted method runs on the inner value, so nothing is overridden
Here is the surprise of the opening, as a program. Base’s Describe calls b.Name(), and Outer, which embeds Base,
declares a Name of its own:
// dispatch.go: a method of an embedded type calls its own methods, never the outer type's.
package main
import "fmt"
type Base struct{}
func (Base) Name() string { return "base" }
func (b Base) Describe() string { return "I am " + b.Name() }
type Outer struct {
Base
}
func (Outer) Name() string { return "outer" }
func main() {
o := Outer{}
fmt.Println(o.Name()) // Outer's own method
fmt.Println(o.Describe()) // promoted from Base: calls Base.Name
} Output
outer I am base
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run dispatch.go
o.Name() returned outer, but o.Describe() printed I am base. Follow the second call:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- o.Name(). Outer declares Name itself, at depth 0, so o.Name() runs Outer.Name and returns outer. Base's Name, one level down, is hidden.
- o.Describe(). Outer has no Describe, so Go finds Base's at depth 1 and calls it with the receiver o.Base, a Base value.
- Inside Describe. Describe calls b.Name(), and b is a Base. The compiler resolved that call on type Base when it compiled Describe, so it runs Base.Name.
- No overriding. Describe returns I am base. Nothing in it knows that an Outer exists; in Go only a call through an interface picks its method at run time.
Describe is Base’s method, compiled with a Base receiver, so the b.Name() inside it always means Base’s Name. In
Java, a call like it inside a superclass reaches the subclass’s override; in Go, the FAQ says, methods on a concrete
type are always resolved statically, and the only way to get dynamic dispatch: Choosing which method a call runs from the type of the value at run time, not from the type written in the code. In Go only calls through an interface are dispatched this way. is
through an interface.
Key idea
Embedding lends a type the fields and methods of another, but every promoted method still runs on the inner value and calls the inner type’s methods.
Code ported from a language with inheritance runs into this first:
Embedding a base type and overriding its methods
Embedding Base and declaring Name on Outer shadows Name for code that calls o.Name(), and for no one else:
Base’s own methods still call Base’s Name. Pass the behaviour in through an interface instead.
Checkpoint
Check: Promoted methods are never overridden
Embedding interfaces, and the API an embedded field exposes Pro: opens with a Pro, Premium or Ultimate pass
A struct can embed an interface as well as a struct, an interface can embed other interfaces, and whatever a type embeds becomes part of what its users can call. This part runs each case, including the nil panic of an empty embedded interface and a lock that leaks into a type’s API.
In this part:
- A struct that embeds an interface hands calls to the value it holds
- An interface that embeds interfaces asks for all their methods
- Embedding a pointer: one part shared, or none at all
- Embedding exports the embedded type’s methods
4 runnable examples (Go) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
From a class hierarchy to composition, case by case Pro: opens with a Pro, Premium or Ultimate pass
Code ported from Java or C# often arrives as a chain of embedded types that is meant to work like inheritance. This part shows where such a chain breaks, rebuilds it from parts and an interface, and weighs the four ways to give a type a part it needs.
In this part:
- The hierarchy that never overrides
- Parts and an interface instead
- Getting dispatch back on purpose
- A test fake that embeds the interface it stands in for
- Four ways to give a type a part
4 runnable examples (Go) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise
The practice questions stay with promotion, shadowing and the missing override, and come back to whichever of the three your answers show is weakest; each choice explains itself:
Practice that adapts to you
Practise this lesson
8 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): Is embedding in Go the same as inheritance?
No. Embedding puts a value of one type inside another and promotes its fields and methods, so the outer type can use them by their short names. But a promoted method still runs on the inner value: when it calls another method, it calls the inner type’s own version, never one the outer type declares. Only a call through an interface chooses its method by the value’s type when the program runs.
Three longer questions of the module bank build on this lesson:
-
Mid to senior
A team ports a Java report generator to Go. BaseReport has a Render method that calls r.Header(), and SalesReport embeds BaseReport and declares its own Header. Every sales report renders the base header. Explain why, and redesign it the Go way.
-
Mid to senior
A shared Cache type embeds sync.Mutex so that its methods can call c.Lock(). Months later a production deadlock is traced to a caller in another package that called Lock on a Cache and never released it. What made that possible, and how do you redesign the type?
-
Mid to senior
A middleware wraps http.ResponseWriter in a struct that embeds it and counts bytes in its own Write. In one code path the wrapper is built with a nil writer and the handler panics, and streaming endpoints behind the middleware stop flushing. Explain both problems and how you build the wrapper safely.
Key takeaways
- An embedded field is declared by its type alone, and its fields and methods are promoted to the outer type, which can then satisfy interfaces through them.
- A selector finds the name at the shallowest depth; the outer type’s own names hide promoted ones, and two names at the same depth are an error only when used.
- A promoted method runs on the inner value and calls the inner type’s methods, so nothing is overridden; only calls through an interface are dispatched at run time.
Terms in this lesson
- dynamic dispatch
- Choosing which method a call runs from the type of the value at run time, not from the type written in the code. In Go only calls through an interface are dispatched this way.
- embedded field
- A struct field declared with a type and no name, such as Audit in struct { Audit; ID string }. The type's name serves as the field's name, and its fields and methods are promoted to the outer struct.
- promoted field
- A field or method of an embedded field that can be used as if the outer struct declared it, such as o.CreatedBy for o.Audit.CreatedBy, unless a name at a shallower depth hides it.
References
- The Go Programming Language Specification: Struct types (The Go Authors)
- The Go Programming Language Specification: Selectors (The Go Authors)
- The Go Programming Language Specification: Interface types (The Go Authors)
- Effective Go: Embedding (The Go Authors)
- Frequently Asked Questions: Why is there no type inheritance? (The Go Authors)
- Frequently Asked Questions: How do I get dynamic dispatch of methods? (The Go Authors)
- Package io: ReadWriter (The Go Authors)
- Package io: WriteString (The Go Authors)
Report a problem with this lesson
Kept only in this browser. Your Learn progress