Embedding and Composition

Go has no inheritance: embedding puts one type inside another and promotes its fields and methods to the outer type.

Syntaxtype 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

Example · go
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
// York

When 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.

Example · go
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 engine

A 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.

Example · go
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"
// 7

A 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.

Example · go
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

  • Be the first to comment on this lesson.