Embedding and Composition
Go has no inheritance. Instead, you build bigger types out of smaller ones — composition — and Go makes that convenient with embedding: put a type inside a struct without a field name, and its fields and methods are promoted so you can use them directly on the outer type. This lesson covers embedding structs and pointers, promotion and shadowing, and why embedding is not the same as inheritance.
Embedding a struct
Write a type name on its own inside a struct to embed it:
package main import ( "fmt" "time" ) type Timestamps struct { Created time.Time Updated time.Time } func (t *Timestamps) Touch(now time.Time) { t.Updated = now } type Article struct { Timestamps // embedded: no field name Title string } func main() { jan := time.Date(2026, 1, 15, 9, 0, 0, 0, time.UTC) a := Article{Title: "Go embedding", Timestamps: Timestamps{Created: jan, Updated: jan}} a.Touch(jan.AddDate(0, 2, 0)) // promoted method fmt.Println(a.Created.Format("2006-01-02")) // promoted field fmt.Println(a.Updated.Format("2006-01-02")) fmt.Println(a.Timestamps.Created.Year()) // the full path still works }
2026-01-15 2026-03-15 2026
The embedded value is still an ordinary field — its name is the type's name, Timestamps — but its fields and methods can be reached directly on Article. Any struct that embeds Timestamps gets the same fields and Touch method for free.
Shadowing
If the outer type declares a field or method with the same name as a promoted one, the outer one wins. The embedded one can still be reached through its full path, which is how an outer method can extend the inner one:
package main import "fmt" type Logger struct{ prefix string } func (l Logger) Log(msg string) { fmt.Println(l.prefix + msg) } type AuditLogger struct { Logger user string } func (a AuditLogger) Log(msg string) { // shadows Logger.Log a.Logger.Log("[" + a.user + "] " + msg) // calls the embedded one } func main() { a := AuditLogger{Logger{"audit: "}, "ana"} a.Log("deleted invoice 42") a.Logger.Log("direct call") }
audit: [ana] deleted invoice 42 audit: direct call
Embedding is not inheritance
An AuditLogger is not a Logger: you can't pass one where a Logger is expected. And when a promoted method runs, its receiver is the inner value — it knows nothing about the outer struct, so there's no "virtual method" that an outer type can override:
package main import "fmt" type Animal struct{ Name string } func (a Animal) Sound() string { return "..." } func (a Animal) Speak() string { return a.Name + " says " + a.Sound() } type Dog struct{ Animal } func (d Dog) Sound() string { return "Woof" } func main() { d := Dog{Animal{"Rex"}} fmt.Println(d.Sound()) // Dog's method fmt.Println(d.Speak()) // Animal.Speak calls Animal.Sound, not Dog.Sound }
Woof Rex says ...
In a language with inheritance, Speak would call the dog's Sound. In Go it doesn't. When you need behaviour that varies by type, use an interface (next lesson) — that's Go's tool for polymorphism.
Embedding several types
package main import "fmt" type Named struct{ Name string } func (n Named) Label() string { return "name: " + n.Name } type Priced struct{ Cents int } func (p Priced) Price() string { return fmt.Sprintf("%d.%02d", p.Cents/100, p.Cents%100) } type Product struct { Named Priced SKU string } func main() { p := Product{Named{"Desk lamp"}, Priced{3450}, "LMP-01"} fmt.Println(p.Label(), "|", p.Price(), "|", p.SKU) }
name: Desk lamp | 34.50 | LMP-01
If two embedded types both provide the same name at the same depth, using that name directly is a compile error (it's ambiguous) — you must write the full path.
Embedding a pointer, and a mutex
You can embed a pointer type as well. A very common pattern embeds a sync.Mutex so the struct gets Lock and Unlock methods (lesson 54):
package main import ( "fmt" "sync" ) type SafeCounter struct { sync.Mutex counts map[string]int } func (c *SafeCounter) Inc(key string) { c.Lock() defer c.Unlock() c.counts[key]++ } func main() { c := SafeCounter{counts: map[string]int{}} c.Inc("visits") c.Inc("visits") fmt.Println(c.counts["visits"]) }
2
Embedding a mutex exposes Lock and Unlock to anyone using the type. In a package's public API, a named, unexported field (mu sync.Mutex) is usually better, so callers can't interfere with your locking.
Embedding interfaces
Interfaces can embed other interfaces to combine them — the standard library's io.ReadWriter is just io.Reader plus io.Writer. You'll see this in the next lesson.
Try it yourself
Create a Contact struct with Email and a Notify method, and embed it in both Customer and Supplier.
to ana@example.com: your order has shipped to orders@acme.test: please restock item 42