Why Go Has No Inheritance
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 34 of 107.
Traditional object-oriented languages like Java and C++ use inheritance to share code between types. A child class extends a parent class, inheriting all its fields and methods. Go deliberately omits this feature.
Inheritance creates tight coupling between types. When a parent class changes, all child classes are affected.
Deep inheritance hierarchies become difficult to understand and maintain. The "fragile base class problem" occurs when modifications to a base class unexpectedly break derived classes.
Go's designers chose a different path: composition over inheritance. Instead of saying "a Dog is an Animal," Go encourages you to say "a Dog has Animal-like behaviors." This subtle shift leads to more flexible, maintainable code.
Go achieves code reuse through two mechanisms you've already learned:
- Interfaces define behavior contracts without implementation details
- Struct embedding allows types to include other types and reuse their methods
Consider this comparison. In traditional OOP, you might write class Dog extends Animal. In Go, you embed an Animal struct inside Dog and implement shared interfaces. The result is similar functionality with looser coupling between types.
This chapter explores struct embedding in depth, showing how Go achieves the benefits of inheritance without its drawbacks.
Challenge
EasyLet's build a notification system that demonstrates Go's composition approach instead of inheritance. You'll create types that share behavior through interfaces and struct embedding rather than class hierarchies.
You'll organize your code across three files:
notifier.go: Define aNotifierinterface with a single methodNotify(message string) string. Also create aBaseNotifierstruct with aNamefield that will be embedded by other types. GiveBaseNotifiera method calledFormat(message string) stringthat returns the message prefixed with the notifier's name in brackets.channels.go: Create two notification channel types that embedBaseNotifierand implement theNotifierinterface:EmailNotifierwith an additionalAddressfieldSMSNotifierwith an additionalPhonefield
Notifymethod should use the embeddedFormatmethod and include its specific channel information in the output.main.go: Create a function calledSendAlertthat accepts anyNotifierand a message, then returns the result of callingNotify. Read the notification details from input, create both types of notifiers, and demonstrate how they can be used interchangeably through the interface.
The following inputs will be provided:
- Line 1: Notifier name for email
- Line 2: Email address
- Line 3: Notifier name for SMS
- Line 4: Phone number
- Line 5: Alert message
Your Format method on BaseNotifier should return:
[Name] messageYour Notify methods should return:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
For example, given Alerts, user@mail.com, Urgent, 555-1234, and Server down, your output should be:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downNotice how both notifier types reuse the Format method from BaseNotifier through embedding, while each provides its own Notify implementation. The SendAlert function works with any Notifier without knowing the concrete type—this is composition over inheritance in action.
Cheat sheet
Go uses composition over inheritance to share code between types, avoiding the tight coupling and fragile base class problems of traditional object-oriented inheritance.
Instead of class hierarchies, Go achieves code reuse through:
- Interfaces - define behavior contracts without implementation details
- Struct embedding - allows types to include other types and reuse their methods
Rather than saying "a Dog is an Animal" (inheritance), Go encourages "a Dog has Animal-like behaviors" (composition).
Struct Embedding Example
Define a base struct that will be embedded:
type BaseNotifier struct {
Name string
}
func (b BaseNotifier) Format(message string) string {
return "[" + b.Name + "] " + message
}Embed the base struct in other types to reuse its fields and methods:
type EmailNotifier struct {
BaseNotifier // embedded struct
Address string
}
func (e EmailNotifier) Notify(message string) string {
// Can call embedded Format method directly
return "Email to " + e.Address + ": " + e.Format(message)
}Define an interface that multiple types can implement:
type Notifier interface {
Notify(message string) string
}Use the interface to work with different concrete types interchangeably:
func SendAlert(n Notifier, message string) string {
return n.Notify(message)
}This approach provides similar functionality to inheritance but with looser coupling between types.
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert accepts any Notifier and a message, returns the result of calling Notify
// TODO: Implement the SendAlert function
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Read email notifier name
scanner.Scan()
emailName := scanner.Text()
// Read email address
scanner.Scan()
emailAddress := scanner.Text()
// Read SMS notifier name
scanner.Scan()
smsName := scanner.Text()
// Read phone number
scanner.Scan()
phoneNumber := scanner.Text()
// Read alert message
scanner.Scan()
alertMessage := scanner.Text()
// TODO: Create an EmailNotifier with emailName and emailAddress
// TODO: Create an SMSNotifier with smsName and phoneNumber
// TODO: Use SendAlert to send the alertMessage through both notifiers
// and print the results
// Suppress unused variable warnings (remove these when you use the variables)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
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