Embedding and Composition
Go has no inheritance: embedding puts one type inside another and promotes its fields and methods to the outer type.
type Customer struct { Address; Name string }Some languages let one type inherit from another, so a Customer can be a kind of Person and get its parts for free. Go has none of that. There are no classes and no subclasses. Instead you build a bigger type by putting smaller types inside it. That is composition, and embedding is the short form of it.
What embedding looks like
An ordinary field has a name and a type: Home Address. An embedded field has only a type: Address. The type's own name then acts as the field name, so the inner value is still reachable as c.Address.
Promotion
The gain is promotion. The embedded type's fields can be read and written as if they were declared on the outer type. If Address has a City field, then c.City works and means exactly c.Address.City. Short form and long form are the same field, so writing through either one changes the same value. Methods are promoted the same way. A method is a function attached to a type and called as v.Name(); declaring them comes later in the course. If Address has a Label method, then c.Label() calls it.
What trips people up
Promotion moves names, not types. A Customer that embeds an Address is not an Address, and a function that takes an Address will not take a Customer; pass c.Address. Names also collide. A field declared directly on the outer struct hides a promoted field of the same name, and the hidden one is still reachable by its long form. Two embedded types that both have a field of the same name, at the same depth, is not an error until something writes that ambiguous selector; then it fails to compile.
Example
package main
import "fmt"
type Address struct {
City string
Code string
}
// Customer embeds Address: a field with a type and no name.
type Customer struct {
Name string
Address
}
func main() {
// In a literal, the embedded field's key is the type name.
c := Customer{Name: "Ada", Address: Address{City: "Leeds", Code: "LS1"}}
// Shorthand: City and Code are promoted onto Customer.
fmt.Println(c.Name, c.City, c.Code)
// Longhand: the same two fields, reached through the embedded value.
fmt.Println(c.Address.City, c.Address.Code)
// Writing through the short form changes the one underlying field.
c.City = "York"
fmt.Println(c.Address.City)
}
// Output:
// Ada Leeds LS1
// Leeds LS1
// YorkWhen to use it
- Give several record types the same block of bookkeeping fields without repeating them.
- Wrap a library type to add fields while keeping its existing methods available.
- Reach a nested value's fields without typing the middle name every time.
More examples
A promoted method
Behaviour is promoted as well as data. The outer type gains the inner type's methods without writing any forwarding code, which is how a type picks up a ready-made ability such as locking or logging. Methods have their own stage later; the point here is only that the promoted call and the long call are identical.
package main
import "fmt"
type Engine struct {
Power int
}
// Describe is a method: a function attached to the Engine type.
func (e Engine) Describe() string {
return fmt.Sprintf("%dhp engine", e.Power)
}
type Car struct {
Engine
Name string
}
func main() {
c := Car{Engine: Engine{Power: 90}, Name: "hatchback"}
// Describe is promoted, so these two calls are the same call.
fmt.Println(c.Describe())
fmt.Println(c.Engine.Describe())
}
// Output:
// 90hp engine
// 90hp engineA field that hides a promoted one
When the outer struct declares a name that the embedded type also has, the outer one is what the short selector means. Nothing warns you. This bites when a library type you embed later gains a field whose name you were already using, so reach for the full path whenever a name appears twice.
package main
import "fmt"
type Base struct {
ID int
}
type Item struct {
Base
ID string
}
func main() {
it := Item{Base: Base{ID: 7}, ID: "sku-7"}
// The field declared on Item itself wins.
fmt.Printf("%q\n", it.ID)
// The hidden field is still reachable by its full path.
fmt.Println(it.Base.ID)
}
// Output:
// "sku-7"
// 7A shared block of fields in several records
Several record types often carry the same housekeeping fields. Declaring them once and embedding that type keeps one definition and still reads as p.Author. Remember that Post is not an Audit: code that wants the shared block as a value takes p.Audit.
package main
import "fmt"
// Audit is a block of bookkeeping fields shared by several records.
type Audit struct {
Author string
Number int
}
type Post struct {
Audit
Title string
}
type Page struct {
Audit
Slug string
}
func main() {
p := Post{Audit: Audit{Author: "ada", Number: 2}, Title: "Hello"}
g := Page{Audit: Audit{Author: "bob", Number: 1}, Slug: "about"}
fmt.Println(p.Author, p.Number, p.Title)
fmt.Println(g.Author, g.Number, g.Slug)
}
// Output:
// ada 2 Hello
// bob 1 about
Discussion