Embedding vs Aggregation
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 38 of 107.
You've learned that embedding promotes fields and methods to the outer struct. But Go also supports aggregation, where you include another struct as a named field. Understanding when to use each approach is key to good design.
With embedding, you omit the field name, and the inner type's members become directly accessible:
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // embedding - no field name
}
d := Document{}
d.Write() // direct access
With aggregation, you give the field an explicit name, requiring that name to access the inner type:
type Document struct {
writer Writer // aggregation - named field
}
d := Document{writer: Writer{}}
d.writer.Write() // must use field name
The key difference is the relationship you're expressing. Embedding suggests an "is-a" relationship where the outer type takes on the identity of the inner type. Aggregation suggests a "has-a" relationship where the outer type merely contains the inner type.
Use embedding when you want the outer type to satisfy interfaces implemented by the inner type, or when direct method access makes the API cleaner. Use aggregation when the inner type is an implementation detail that shouldn't be exposed, or when you want explicit control over how the inner type is accessed.
Challenge
EasyLet's build a report generation system that demonstrates when to use embedding versus aggregation. You'll create a system where some components should be directly accessible (embedding) while others should remain internal implementation details (aggregation).
You'll organize your code across three files:
formatter.go: Create aFormatterstruct with aStylefield (string) and aFormat(text string) stringmethod that returns the text wrapped with the style, like[Style] text [/Style]. This will be an internal helper that shouldn't be directly exposed.report.go: Create two structs that handle report generation differently:- A
Metadatastruct withAuthorandDatefields—this represents public information that should be directly accessible - A
Reportstruct with aTitlefield that embedsMetadata(for direct access to author and date) but uses aggregation forFormatter(as an internal implementation detail with a named fieldformatter)
Generate() stringmethod toReportthat uses the internal formatter to format the title and includes the metadata.- A
main.go: Read report details from input, create aReportwith both embedded metadata and an aggregated formatter. Demonstrate the difference by accessing the author directly on the report (through embedding) and then generating the formatted report (which uses the hidden formatter internally).
The following inputs will be provided:
- Line 1: Report title
- Line 2: Author name
- Line 3: Date
- Line 4: Formatter style
Your Format method should return:
[Style] text [/Style]Your Generate method should return:
[formatted title]
Author: [Author], Date: [Date]In your main file, print three lines:
- The author accessed directly on the Report (demonstrating embedding promotes the field)
- The date accessed through the embedded type name (
report.Metadata.Date) - The result of calling
Generate()
For example, given Quarterly Results, Jane Doe, 2024-01-15, and BOLD, your output should be:
Jane Doe
2024-01-15
[BOLD] Quarterly Results [/BOLD]
Author: Jane Doe, Date: 2024-01-15Notice how Author and Date are accessible directly on the Report through embedding, while the formatter remains hidden—you can't call report.Format() directly because it's aggregated with a named field. This demonstrates the "is-a" versus "has-a" relationship in practice.
Cheat sheet
Go supports two ways to include one struct within another: embedding and aggregation.
Embedding omits the field name, promoting the inner type's fields and methods to the outer struct:
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // embedding - no field name
}
d := Document{}
d.Write() // direct access to embedded method
Aggregation uses an explicit field name, requiring that name to access the inner type:
type Document struct {
writer Writer // aggregation - named field
}
d := Document{writer: Writer{}}
d.writer.Write() // must use field name
When to use each:
- Embedding: Expresses an "is-a" relationship. Use when the outer type should satisfy interfaces of the inner type, or when direct method access improves the API.
- Aggregation: Expresses a "has-a" relationship. Use when the inner type is an implementation detail that shouldn't be exposed, or when you want explicit control over access.
With embedding, you can still access fields through the embedded type name if needed:
type Metadata struct {
Author string
}
type Report struct {
Metadata // embedded
}
r := Report{Metadata: Metadata{Author: "Jane"}}
r.Author // direct access via promotion
r.Metadata.Author // explicit access via type name
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
title := scanner.Text()
scanner.Scan()
author := scanner.Text()
scanner.Scan()
date := scanner.Text()
scanner.Scan()
style := scanner.Text()
// TODO: Create a Metadata struct with the author and date
// TODO: Create a Formatter with the given style
// TODO: Create a Report that embeds Metadata and aggregates Formatter
// TODO: Print the author accessed directly on the Report (embedding promotes the field)
// TODO: Print the date accessed through the embedded type name (report.Metadata.Date)
// TODO: Print the result of calling Generate()
// Suppress unused variable warnings (remove these when you use the variables)
_ = title
_ = author
_ = date
_ = style
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool