Observer Pattern
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 89 of 107.
The Observer pattern defines a one-to-many relationship between objects. When one object (the subject) changes state, all its dependents (observers) are notified automatically. This is ideal for event systems, notifications, or any scenario where multiple components need to react to changes.
In Go, we implement this pattern using an interface for observers and a subject struct that manages subscriptions:
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}Any type implementing the Observer interface can subscribe to receive notifications:
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}The subject notifies all observers without knowing their concrete types:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// Email to user@example.com: Order placed
// Log: Order placedThis pattern decouples the subject from its observers, making it easy to add new observer types without modifying existing code. It's commonly used in event-driven architectures, GUI frameworks, and pub/sub messaging systems.
Challenge
EasyLet's build a stock price alert system using the Observer pattern! You'll create a system where multiple alert services automatically receive notifications whenever a stock price changes—perfect for demonstrating how observers can react to events without the subject knowing their concrete types.
You'll organize your code across three files:
observer.go: Define your observer interface and concrete observer types.Create an
Observerinterface with a methodUpdate(stockName string, price float64)that receives stock information when prices change.Implement two observer types:
PriceAlertwith aThresholdfield (float64)—when updated, it returnsALERT: [stockName] at $[price] crossed threshold $[threshold]if the price is above the threshold, orWATCHING: [stockName] at $[price] (threshold: $[threshold])if belowPriceLoggerwith aLogNamefield (string)—when updated, it returns[logName] recorded [stockName]: $[price]
Both observers should have a method that returns their formatted response string.
stock.go: Create your subject that manages observers and notifies them of price changes.Build a
Stockstruct with fields forName(string),Price(float64), and a slice of observers. Add these methods:Subscribe(o Observer)to add an observerSetPrice(price float64) []stringthat updates the price and notifies all observers, returning a slice of all their responses
Also create a
NewStock(name string, initialPrice float64) *Stockconstructor.main.go: Set up your stock with observers and simulate price changes.Read the stock name and initial price. Then read a count of observers to create. For each observer, read its type (
alertorlogger) and its configuration (threshold for alerts, log name for loggers). Subscribe each observer to the stock.Finally, read a new price, update the stock, and print each observer's response on its own line.
The following inputs will be provided:
- Line 1: Stock name
- Line 2: Initial price
- Line 3: Number of observers
- For each observer: type (
alertorlogger), then configuration value - Final line: New price to set
Format all prices with two decimal places.
For example, given:
GOOG
150.00
3
alert
155.00
logger
TradeLog
alert
140.00
160.50Your output should be:
ALERT: GOOG at $160.50 crossed threshold $155.00
TradeLog recorded GOOG: $160.50
ALERT: GOOG at $160.50 crossed threshold $140.00And given:
AAPL
180.00
2
logger
DailyLog
alert
200.00
175.25Your output should be:
DailyLog recorded AAPL: $175.25
WATCHING: AAPL at $175.25 (threshold: $200.00)Notice how the Stock doesn't know whether it's notifying alerts or loggers—it just calls Update on each observer. This decoupling makes it easy to add new observer types without modifying the Stock code!
Cheat sheet
The Observer pattern defines a one-to-many relationship where a subject notifies multiple observers automatically when its state changes. This decouples the subject from its observers, making it ideal for event systems and notifications.
Basic Structure
Define an observer interface and a subject that manages subscriptions:
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}Implementing Observers
Any type implementing the Observer interface can subscribe:
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}Usage
Subscribe observers and notify them of changes:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// Email to user@example.com: Order placed
// Log: Order placedThe subject notifies all observers without knowing their concrete types, making it easy to add new observer types without modifying existing code.
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Read stock name
stockName, _ := reader.ReadString('\n')
stockName = strings.TrimSpace(stockName)
// Read initial price
initialPriceStr, _ := reader.ReadString('\n')
initialPrice, _ := strconv.ParseFloat(strings.TrimSpace(initialPriceStr), 64)
// Read number of observers
numObserversStr, _ := reader.ReadString('\n')
numObservers, _ := strconv.Atoi(strings.TrimSpace(numObserversStr))
// TODO: Create a new Stock using NewStock constructor
// TODO: Loop through each observer
for i := 0; i < numObservers; i++ {
// Read observer type ("alert" or "logger")
observerType, _ := reader.ReadString('\n')
observerType = strings.TrimSpace(observerType)
// Read configuration value
configValue, _ := reader.ReadString('\n')
configValue = strings.TrimSpace(configValue)
// TODO: Create appropriate observer based on type
// - For "alert": parse configValue as float64 threshold, create PriceAlert
// - For "logger": use configValue as LogName, create PriceLogger
// TODO: Subscribe the observer to the stock
}
// Read new price
newPriceStr, _ := reader.ReadString('\n')
newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)
// TODO: Set the new price and get responses
// TODO: Print each response on its own line
}
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 Collection13Design Patterns Part 1
Intro to Design PatternsSingleton PatternFactory PatternAbstract Factory PatternObserver PatternStrategy Pattern2Types & 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