Composition with structures and method forwarding in Go

Gene Kuo
4 min readOct 12, 2021
Photo by Finn Gerkens on Unsplash

In object-oriented programming, we compose objects from smaller objects to model the problem at hand. In the same way, in Go, we use composition with structures. In this article, I will talk about a language feature called structure embedding of Go and some careful considerations, when we apply composition with structures.

Designing software with composition

Composition is considered more flexible, greater reuse, and easier changes than software built with inheritance. We design software by breaking large structures into smaller structures and composing them together. Those smaller can be independently used and composed in many other ways, hence enhancing flexibility.

For example, a solution to compose a simple report in Go can be model as follows:

type celsius float64type temperature struct {
maximum, minimum celsius
}
type location struct {
latitude, longitude float64
}
type report struct {
sol int
temperature temperature
location location
}

We can then build a report as follows:

loc := location{-41.7856, 120.5623}
temp := temperature{maximum: 35.0, minimum: 12.0}
report := report{sol: 30, temperature: temp, location: loc}
fmt.Printf("The maximum temperature is %v° C\n", report.temperature.maximum)

With those types defined, we can further attach methods to each type and use them independently.

func (t temperature) average() celsius {
return (t.maximum + t.minimum) / 2
}
// Use the method independently
fmt.Printf("Average temperature is %v° C\n", temp.average())
// Or use the method from the report
fmt.Printf("Average temperature in the report is %v° C\n", report.temperature.average())

Method forwarding

In Go, when we design software using composition, we can manually write methods to forward from one type to another as follows:

func (r report) average() celsius {
return r.temperature.average()
}
// Use it through the report
fmt.Printf("Average temperature in the report is %v° C\n", report.average())

However, Go provides method forwarding for you with struct embedding to reduce the repetitive code we have to write. We just need to embed a type in a structure without specifying a field name in that structure.

type report struct {
sol int
temerature
location
}

Even though we don’t give a name for the embedded field, Go automatically gives the field name the same as the type of the structure. So we can access the field and its methods as follows:

fmt.Printf("Average temperature in the report is %v° C\n", report.temperature.average())

More importantly, Go allows fields of an inner structure accessible from the outer structure, and we can call the methods of those fields from the outer structure.

fmt.Printf("The maximum temperature is %v° C\n", report.maximum)
report.minimum = 10

Any type can be embedded in structure in Go, including alias types like sol .

Naming Collisions

If both temperature and location fields have the same methods such as display(), we have an ambiguous selector when we call report.display() .

func (t temperature) display() string {
return fmt.Sprintf("Maximum temperature: %v° C, Minimum temperature: %v° C\n", t.maximum, t.minimum)
}
func (l location) display() string {
return fmt.Sprintf("logitude: %v, latitude: %v\n", l.latitude, l.longitude)
}
// call report.display() will cause error: ambiguous selector
// report.display
fmt.Printf("Disply report: %v\n", report.display())

To resolve an ambiguous selector, we can implement the display method on the report type, and it will take precedence over the same method from the embedded types.

func (r report) display() string {
return fmt.Sprintf("Maximum temperature: %v° C,
Minimum temperature: %v° C at
(latitude: %v, latitude: %v)\n", r.maximum, r.minimum,
r.latitude, r.longitude)
}

Careful considerations about embedding fields

Because embedded fields are used to promote the fields and methods of the inner structure, the outer structure implicitly satisfies the interfaces that the inner structure implements. When the outer structure is used in a function or method such as parameter passing, the function or method will forward to the implementation of the inner structure. This might result in incorrect behavior that the function or method performed. The following illustrates this point.

package mainimport (
"fmt"
"encoding/json"
"time"
"os"
)
type Event struct {
ID int
time.Time
}
func main() {
event := Event{
ID: 6735,
Time: time.Now(),
}
second := event.Second()
fmt.Printf("second: %d\n", second)
b, err := json.Marshal(event)
if err != nil {
os.Exit(1)
}
fmt.Printf("json: %s\n", string(b))
}

The result shows that event being marshaled doesn’t include the value of the ID field, which is not what we expect.

The reason is as follows. Through structure embedding, we promote the time.Time fields and methods to the Event structure. time.Time implements json.Marshaler interface and provide the required MarshalJSON method implementation to override the default marshaling. Now, Event also implicitly satisfies the json.Marshaler interface. When we pass event to json.Marshal, the method implementation will not use the default behavior but the one provided by the time.Time through method forwarding. So only time.Time filed is marshaled.

// type declaration in package encoding/json
type Marshaler interface {
MarshalJSON() ([]byte, error)
}

One solution is to provide a custom implementation of the interface promoted in the outer structure to override the implementation of the same interface of the inner structure. The other solution is to not embed the field for the inner structure.

type Event struct {
ID int
Time time.Time
}

Summary

Classical languages like Java, C#, and Python can use composition and inheritance to design software. However, there isn’t inheritance in Go.

Structure embedding in Go looks like inheritance in classical languages, but it is not. It is about structure composition and method forwarding at work.

Structure embedding provides convenience for writing software in Go and making use of composition. It helps reduce boilerplate code and enhance flexibility.

We must consider carefully when we design our program using composition and structure embedding, with the structures we defined and those from third parties to avoid unexpected behavior.

Thanks for reading.

--

--

Gene Kuo

Solutions Architect, AWS CSAA/CDA: microservices, kubernetes, algorithms, Java, Rust, Golang, React, JavaScript…