Grokking Advanced Go
Vote

0% completed

​

Embedding and Method Promotion

Promoted Fields and Methods

Selector Depth and Shadowing

Embedding and Inheritance

Embedding a Pointer

Embedding an Interface

Key Takeaways

Both structs below add one piece of audit information to an Order. The first keeps the Order in a named field, while the second lists only its type. The two versions hold the same data, and they differ only in how calling code reaches it.

Go
Go

. . . .

Output:

1001 2500 1001 2500 1001 2500

The second line reaches ID and Total directly on the outer value, and the third shows that the longer form still works. No method was written on Audited, and no field was copied.

This shortcut is easy to read as inheritance, and that reading leads to real bugs. What exactly does embedding do, what are its limits, and when is it the right tool?

Promoted Fields and Methods

A field declared with a type and no name is an embedded field, and its name is the type name. The fields and methods of the embedded type are promoted, which means that they can be selected directly on the outer type. The expression embedded.Total() is a shorter spelling of embedded.Order.Total(), and the compiler treats it that way.

Promotion also counts when the compiler checks whether a type satisfies an interface. A struct that embeds T has the methods of T in its own method set, and the pointer to that struct has the methods of both T and *T. A struct that embeds *T has all of them in both forms. The rule follows from what can be addressed. It is the same rule that decides which methods a non-embedded value has.

Promotion also brings in methods that you did not intend to expose. The program below embeds a Customer that has a String method. The Order now satisfies fmt.Stringer through promotion, so fmt calls that method and prints only the customer.

Go
Go

. . . .

Output:

Ada 1001 Ada

The ID has disappeared from the printed order, although no code asked for that. The same effect occurs with Error, or with any other method that a library looks for. Embedding a type adds all of its exported methods to the exported API of the outer type. For example, embedding a sync.Mutex in an exported struct gives that struct exported Lock and Unlock methods, so a named field is the usual choice there.

Selector Depth and Shadowing

The depth of a field or method is the number of embedded fields that the compiler must pass through to reach it. A name declared directly on the struct has depth zero, and a name in a type embedded once has depth one. When a selector could mean several things, the Go specification chooses the one at the shallowest depth.

Go
Go

. . . .

Output:

S-7 shipment S-7 1001 order 1001

The outer ID and Label shadow the inner ones, which means that the shorter selector now finds the outer names. The inner ones still exist, and the full path still reaches them. The two fields may even have different types, as they do here.

When the same name appears twice at the shallowest depth, the specification selects neither. The compiler accepts the declaration and reports an error only where the code uses the ambiguous name.

type Shipment struct { ID int64 } type Combined struct { Order Shipment } var c Combined fmt.Println(c.ID)
./main.go:20:16: ambiguous selector c.ID

This has a consequence for library upgrades. If a new release of an embedded type adds a method whose name matches a method in another embedded type at the same depth, then code that uses that name stops compiling. Interface satisfaction can also disappear without any error at the declaration, because an ambiguous method is not promoted. A struct that embeds two types with String methods has no String method of its own, so fmt prints its fields instead.

Embedding and Inheritance

In a language with inheritance, a method of a base class can call a method that a derived class overrides, which is called virtual dispatch. Go has no such mechanism for struct embedding. A promoted method runs with the inner value as its receiver, and that receiver holds no reference to the struct that contains it.

Go
Go

. . . .

Output:

rush order order 1001

The call rush.Describe() is rush.Order.Describe(), so the receiver inside Describe is an Order. Its call to o.Kind() therefore finds Order.Kind, and RushOrder.Kind is never called. An engineer who expects inheritance would predict rush order 1001 here. To let the inner code call outer behavior, pass that behavior in explicitly, as a function or as an interface value.

Embedding also creates no subtype relationship. A RushOrder cannot be passed where an Order is required, so a call to a function ship(o Order) is rejected. The repair is to pass the inner value by name, as in ship(rush.Order).

ship(RushOrder{Order{ID: 1001}})
./main.go:16:7: cannot use RushOrder{…} (value of struct type RushOrder) as Order value in argument to ship
InheritanceGo embedding
Inner code can call an outer overrideYesNo
Outer value usable as the inner typeYesNo, select the field explicitly
RelationshipThe outer type is a kind of the inner typeThe outer type contains the inner value
Method receiverThe whole objectThe inner value only
Where a promoted method runs
Where a promoted method runs

Embedding a Pointer

An embedded field can be a pointer, written *Order. Promotion works the same way, but the outer struct then shares the inner value with anything else that holds the pointer, as Value and Pointer Semantics described. Several wrappers can therefore refer to one Order.

The zero value of the outer struct holds a nil pointer. Reading a promoted field through it panics, even though the selector does not show any pointer.

Go
Go

. . . .
1002 panic: runtime error: invalid memory address or nil pointer dereference

Embedding an Interface

A struct can also embed an interface. The struct then has all the methods of that interface, and each call goes to whatever value the field holds. This allows a wrapper that changes one method and passes every other method through without any further code (a decorator).

Go
Go

. . . .

Output:

<nil> {1001} <nil> <nil> 1

The wrapper has one method of its own, yet it satisfies the full Store interface. Without embedding, it would need a forwarding method for Find and another for Delete, and one more each time the interface grew.

The same risk exists here as with a pointer. If the field is left unset, the struct still compiles and still satisfies the interface, and the failure occurs only at the first call that reaches the field. In the program above, (&auditedStore{}).Find(1001) panics with the nil pointer message, because the embedded interface holds no value. Always set the field in a constructor function, so that callers receive a wrapper that already has its inner value.

Key Takeaways

  • An embedded field promotes the fields and methods of its type, so they can be selected directly on the outer type.
  • Promoted methods count toward interface satisfaction, and they join the exported API of the outer type.
  • A selector finds the name at the shallowest depth, and two names at that depth cause a compile error only where they are used.
  • A promoted method runs on the inner value, so inner code never calls an outer override.
  • An outer type is not usable as its inner type, because embedding creates no subtype relationship.
  • An embedded pointer or interface that is left unset causes a nil pointer panic at the first use.

Embedding places one value inside another and adds shorter names for what the inner value offers. The inner value remains an independent value with no reference to its container. Used with that understanding, embedding removes forwarding code and allows small wrappers. A named field is the better choice whenever the outer type should not expose the inner type's full API.

Reading Progress

0%


Vote for new content

On This Page

Promoted Fields and Methods

Selector Depth and Shadowing

Embedding and Inheritance

Embedding a Pointer

Embedding an Interface

Key Takeaways