Welcome back, Gophers ❤. If you’ve followed along from our previous dives, you know that building a system isn’t just about spawning objects or making them fit together. In the real world, things change. Objects need to talk, react, argue, delegate, and remember.
Behavioral Design Patterns are the protocols of communication. They don’t care about how an object is built or where it sits in the hierarchy. They care about intent. How does a change in one corner of your app ripple through to the other? How do you avoid a switch statement that grows into a 2,000 line monster?
In this final installment, we aren’t just writing code; we are choreographing a dance. We’re going to make your Go code feel less like a rigid machine and more like a living, breathing ecosystem.
The Roadmap for Part 3
This isn’t a quick read. This is a “grab a coffee and sit on the balcony” kind of read. Here is the map for our final expedition into the social dynamics of Go:
- The Deciders: Strategy and State.
- The Communicators: Observer and Mediator.
- The Workers: Command and Chain of Responsibility.
- The Organizers: Iterator and Visitor.
- The Philosophers: Template Method and Memento.
Why Behavioral Patterns in Go?
Let’s be honest. Go isn’t Java. We don’t have traditional class inheritance. We have Interfaces and Composition. Because of this, Behavioral patterns in Go look… different. They are cleaner. They rely on Go’s unique implicitly implemented interfaces and powerful channels.
When you apply these patterns correctly in Go, you stop writing spaghetti code and start writing legos. You can pull a piece out, swap it for another, and the rest of the system doesn’t even blink.
Before We Dive In…
We’ve come a long way from the Singleton in Part 1. We’ve moved past the Facade in Part 2. Now, we are entering the world of Runtime Dynamics.
Ready to level up from a developer to a true Software Architect? Let’s begin our first step into the most common and powerful behavioral tool: The Strategy Pattern.
1. Strategy Pattern:
Imagine you are building a high-scale e-commerce engine. You have a Checkout process. Everything is going great until the marketing team says: “We need to support Credit Cards.” Then a week later: “We need PayPal.” Then: “Crypto is the future, add Bitcoin.” Without the Strategy Pattern, your Checkout function becomes a graveyard of if-else or switch statements. Every time you add a new payment method, you risk breaking the entire checkout flow.
The Strategy Pattern lets you define a family of algorithms (payment methods), put each of them into a separate struct, and make them interchangeable. The checkout process doesn’t care how you pay; it just knows it needs to call a Pay method
A) The Architecture:
B) Go Implementation: The Shopping Cart
Implementing the Strategy pattern in Go feels incredibly natural because of implicit interfaces. Unlike other languages, your structs don’t need a specific implements keyword to link them to an interface; if they have the required method, they simply work. In the example below, we treat our payment methods as plug-and-play components that the ShoppingCart uses without needing to know their internal secrets.
package main
import "fmt"
// 1. The Strategy Interface
// This is the Contract. Any new payment method must follow this.
type PaymentStrategy interface {
Pay(amount int) string
}
// 2. Concrete Strategy: Credit Card
type CreditCard struct {
CardNumber string
CVV string
}
func (cc *CreditCard) Pay(amount int) string {
return fmt.Sprintf("Paid %d using Credit Card (No: %s)", amount, cc.CardNumber)
}
// 3. Concrete Strategy: PayPal
type PayPal struct {
Email string
}
func (pp *PayPal) Pay(amount int) string {
return fmt.Sprintf("Paid %d using PayPal (Email: %s)", amount, pp.Email)
}
// 4. The Context: ShoppingCart
// It doesn't know WHICH strategy it's using. It just uses "PaymentStrategy".
type ShoppingCart struct {
TotalAmount int
PaymentMethod PaymentStrategy
}
func (sc *ShoppingCart) SetPaymentMethod(method PaymentStrategy) {
sc.PaymentMethod = method
}
func (sc *ShoppingCart) Checkout() {
if sc.PaymentMethod == nil {
fmt.Println("Error: Please select a payment method!")
return
}
result := sc.PaymentMethod.Pay(sc.TotalAmount)
fmt.Println(result)
}
func main() {
cart := &ShoppingCart{TotalAmount: 500}
// Dynamic decision at runtime: User chooses Credit Card
cart.SetPaymentMethod(&CreditCard{CardNumber: "1234-5678", CVV: "123"})
cart.Checkout()
// Later, user changes mind and chooses PayPal
cart.SetPaymentMethod(&PayPal{Email: "gopher@golang.org"})
cart.Checkout()
}
C) The Architect’s Perspective:
- Open/Closed Principle: You can add 50 new payment methods (Apple Pay, Stripe, Klarna) without changing a single line of code in the ShoppingCart or the Checkout logic.
- Testability: You can easily pass a “MockPayment” strategy to test your checkout logic without actually hitting a bank API.
- Clean Code: No more giant switch blocks. Each payment logic lives in its own small, manageable file.
While Strategy lets us swap algorithms from the outside, sometimes an object needs to change its behavior from the inside based on its own mood or status. That brings us to the internal logic of the State Pattern*.*
2. State Pattern:
If the Strategy pattern is about you (the client) choosing a tool, the State Pattern is about the object itself deciding how to act based on its internal mood.
Imagine a Vending Machine. If you press the dispense button when you haven’t put any money in, it does nothing. If you’ve inserted a coin, it gives you a soda. If the machine is out of stock, it returns your coin. The dispense button is the same, but the machine’s behavior depends entirely on its internal state.
In Go, we use this to eliminate the conditional hell, those massive switch or if-else blocks that check if doc.Status == "Draft" or if doc.Status == "Moderation". Instead, we let the status be its own object.
A) The Architecture:
B) Go Implementation: The Vending Machine
package main
import "fmt"
// 1. The State Interface
type State interface {
RequestItem() error
DispenseItem() error
}
// 2. The Context: VendingMachine
type VendingMachine struct {
hasItemState State
noItemState State
itemCount int
currentState State
}
func (v *VendingMachine) RequestItem() {
err := v.currentState.RequestItem()
if err != nil {
fmt.Println(err)
return
}
v.currentState.DispenseItem()
}
func (v *VendingMachine) setState(s State) {
v.currentState = s
}
// 3. Concrete State: HasItem (The machine is ready)
type HasItemState struct {
vendingMachine *VendingMachine
}
func (i *HasItemState) RequestItem() error {
if i.vendingMachine.itemCount == 0 {
i.vendingMachine.setState(i.vendingMachine.noItemState)
return fmt.Errorf("Item out of stock")
}
fmt.Println("Item requested")
return nil
}
func (i *HasItemState) DispenseItem() error {
fmt.Println("Dispensing item")
i.vendingMachine.itemCount--
if i.vendingMachine.itemCount == 0 {
i.vendingMachine.setState(i.vendingMachine.noItemState)
}
return nil
}
// 4. Concrete State: NoItem (Out of stock)
type NoItemState struct {
vendingMachine *VendingMachine
}
func (i *NoItemState) RequestItem() error {
return fmt.Errorf("Item out of stock")
}
func (i *NoItemState) DispenseItem() error {
return fmt.Errorf("Item out of stock")
}
func main() {
vendingMachine := &VendingMachine{
itemCount: 1,
}
hasItemState := &HasItemState{vendingMachine: vendingMachine}
noItemState := &NoItemState{vendingMachine: vendingMachine}
vendingMachine.hasItemState = hasItemState
vendingMachine.noItemState = noItemState
// Set initial state
vendingMachine.setState(hasItemState)
// First request: Success
vendingMachine.RequestItem()
// Second request: Fails because it's now in NoItemState
vendingMachine.RequestItem()
}
C) The Engineering Edge:
- Single Responsibility: Each state-specific behavior is isolated in its own struct.
- Encapsulation: The machine doesn’t need to check how many items are left every time a button is pressed; the state transitions handle that logic automatically.
- Cleaner Transitions: Adding a MaintenanceState or PowerOffState doesn’t require rewriting the
RequestItemfunction.
Now that our object can manage its internal changes effectively, how do we let the rest of the system know when something important has actually happened? Let’s tune into the notification broadcast of the Observer Pattern.
3. Observer Pattern:
In the e-commerce world, when an order is shipped, many things need to happen: the customer gets an email, the database updates, and the warehouse logs the exit. If you put all this code inside your ShipOrder() function, you've just built a god function that is impossible to maintain.
The Observer Pattern allows a subject to maintain a list of observers (Email, Warehouse, Analytics). When the subject changes, it shouts out to the list, and everyone reacts in their own way.
A) The Architecture:
B) Go Implementation: The Order Notification System
package main
import "fmt"
// 1. The Observer Interface
type Observer interface {
Update(string)
}
// 2. The Subject (Publisher)
type OrderManager struct {
observers []Observer
}
func (o *OrderManager) Register(observer Observer) {
o.observers = append(o.observers, observer)
}
func (o *OrderManager) Notify(orderID string) {
for _, observer := range o.observers {
observer.Update(orderID)
}
}
// 3. Concrete Observer: Email Service
type EmailService struct{}
func (e *EmailService) Update(orderID string) {
fmt.Printf("Email Service: Sending confirmation for order %s\n", orderID)
}
// 4. Concrete Observer: Warehouse Service
type WarehouseService struct{}
func (w *WarehouseService) Update(orderID string) {
fmt.Printf("Warehouse: Preparing items for order %s\n", orderID)
}
func main() {
manager := &OrderManager{}
// Registering different departments
manager.Register(&EmailService{})
manager.Register(&WarehouseService{})
fmt.Println("New order received!")
manager.Notify("ORDER_7788")
}
C) Gopher Wisdom:
- Decoupling: The
OrderManagerdoesn't know theEmailServiceexists. It just knows it has a list of things that want to be updated. - Scalability: Need to add a Slack notification for the team? Just create a
SlackObserverstruct and register it. You don't have to touch a single line of the existingOrderManagercode. - Asynchronicity: In a real-world Go app, a senior developer would often run the
Notifyloop in separate goroutines or use channels, allowing the main process to continue without waiting for every observer to finish.
Observers are great for broadcasts, but when too many objects start shouting at each other, the network becomes a chaotic web of dependencies. To bring order to this chaos, we need an air traffic controller: the Mediator Pattern.
4. Mediator Pattern:
As your application grows, you’ll find that objects start knowing too much about each other. A button needs to talk to a text field, which needs to talk to a checkbox, which needs to trigger a save operation. This leads to a spaghetti dependency where every object is connected to every other object (O(N²) complexity).
The Mediator Pattern solves this by forcing objects to stop talking to each other directly. Instead, they all talk to a central Mediator. Think of it as an Air Traffic Control Tower: pilots don’t call other pilots to coordinate landings; they all talk to the tower. The tower manages the traffic, and the pilots just follow instructions.
A) The Architecture:
B)Go Implementation: Air Traffic Control
In Go, the Mediator pattern is powerful because it allows us to keep our plane structs very slim. They don’t need to know about other planes; they only need to hold a reference to the Mediator interface.
package main
import "fmt"
// 1. The Mediator Interface
type Mediator interface {
Notify(sender string, event string)
}
// 2. The Components (Colleagues)
type Plane struct {
callSign string
mediator Mediator
}
func (p *Plane) RequestLanding() {
fmt.Printf("Plane %s: Requesting permission to land.\n", p.callSign)
p.mediator.Notify(p.callSign, "landing_requested")
}
func (p *Plane) Land() {
fmt.Printf("Plane %s: I am landing. Clear the runway!\n", p.callSign)
}
// 3. The Concrete Mediator
type ControlTower struct {
planes map[string]*Plane
}
func (c *ControlTower) RegisterPlane(p *Plane) {
c.planes[p.callSign] = p
}
func (c *ControlTower) Notify(sender string, event string) {
if event == "landing_requested" {
fmt.Printf("Tower: Received landing request from %s. Checking runway...\n", sender)
// The tower coordinates: Telling other planes to hold
for callSign, plane := range c.planes {
if callSign != sender {
fmt.Printf("Tower: Telling %s to circle and wait.\n", callSign)
}
}
// Permission granted to the sender
c.planes[sender].Land()
}
}
func main() {
tower := &ControlTower{planes: make(map[string]*Plane)}
plane1 := &Plane{callSign: "GopherAir-101", mediator: tower}
plane2 := &Plane{callSign: "SkyGo-502", mediator: tower}
tower.RegisterPlane(plane1)
tower.RegisterPlane(plane2)
// Plane 1 initiates the request through the mediator
plane1.RequestLanding()
}
C) Why This Wins at Scale:
- Reduced Coupling: The planes are completely oblivious to each other. You could have 1,000 planes, and none of them would need to store a list of the other 999.
- Centralized Logic: If the rules for landing change (e.g., emergency priority), you only change the logic in the
ControlTower, not in every singlePlanestruct. - Ease of Reuse: Because the
Planeonly depends on aMediatorinterface, you can reuse the samePlanelogic in a different "World" or simulation just by passing a different mediator.
Centralizing communication is key, but what if we want to package the actual request itself into a standalone, portable object? It’s time to wrap our intents into capsules with the Command Pattern.
5. Command Pattern:
In a growing application, you often want to trigger actions from different places. For example, a save action could be triggered by a button, a keyboard shortcut (Ctrl+S), or an auto-save timer. If you hardcode the save logic into the button, you’ll have to duplicate it for the shortcut and the timer.
The Command Pattern solves this by turning the request into a stand-alone object. Think of a Restaurant Order Slip: the waiter (Invoker) doesn’t need to know how to cook a pizza; they just write the request on a slip (Command) and hand it to the kitchen. The chef (Receiver) eventually takes the slip and performs the work. This allows you to queue requests, log them, or even undo them later.
A) The Architecture:
B) Go Implementation: The Universal Remote
In Go, the Command pattern is incredibly clean. Since we use interfaces, the Invoker (the Remote Control) doesn’t need to know if it’s turning on a light, a TV, or an air conditioner. It just calls Execute()
package main
import "fmt"
// 1. The Command Interface
type Command interface {
Execute()
}
// 2. The Receiver: Light
// This is the object that actually knows HOW to do the work.
type Light struct {
isOn bool
}
func (l *Light) On() {
l.isOn = true
fmt.Println("Light is ON")
}
func (l *Light) Off() {
l.isOn = false
fmt.Println("Light is OFF")
}
// 3. Concrete Command: Turn Light On
type LightOnCommand struct {
light *Light
}
func (c *LightOnCommand) Execute() {
c.light.On()
}
// 4. Concrete Command: Turn Light Off
type LightOffCommand struct {
light *Light
}
func (c *LightOffCommand) Execute() {
c.light.Off()
}
// 5. The Invoker: Remote Control
type RemoteControl struct {
command Command
}
func (r *RemoteControl) SetCommand(c Command) {
r.command = c
}
func (r *RemoteControl) PressButton() {
r.command.Execute()
}
func main() {
// Create the Receiver
livingRoomLight := &Light{}
// Create Concrete Commands
lightOn := &LightOnCommand{light: livingRoomLight}
lightOff := &LightOffCommand{light: livingRoomLight}
// Create the Invoker
remote := &RemoteControl{}
// Turning the light ON
remote.SetCommand(lightOn)
remote.PressButton()
// Turning the light OFF
remote.SetCommand(lightOff)
remote.PressButton()
}
C) The Clean Code Advantage:
- Undo/Redo Capability: By storing a history of Command objects in a stack, you can easily implement “Undo” by adding an
Unexecute()method to the interface. - Command Queueing: You can store commands in a slice and process them one by one. This is perfect for background tasks or job processors.
- Decoupling Sender and Receiver: The
RemoteControl(Sender) has zero knowledge of theLight(Receiver). This makes your UI code completely independent of your business logic.
A Command carries a specific intent, but what if that intent needs to pass through a series of checkpoints or filters before being executed? Let’s build a relay race of logic with the Chain of Responsibility Pattern.
6. Chain of Responsibility Pattern:
Imagine you are calling technical support. First, you talk to an automated bot. If the bot can’t solve your “how do I reset my password” question, it passes you to a Level 1 Operator. If your problem is a complex server crash, the operator passes you to a Senior Engineer. This is the Chain of Responsibility.
In software, we use this when an order or a request needs to go through multiple stages like authentication, logging, validation, and caching. Instead of one giant function doing everything, you create a chain of independent handlers. Each handler does its job and decides: “Do I solve this now, or do I pass it to the next person?”.
A) The Architecture:
B) Go Implementation: The Middleware Chain
In Go, this pattern is the secret sauce behind almost every major web framework’s Middleware. Each step is a handler that decides whether to stop the request or let it flow to the next one.
package main
import "fmt"
// 1. The Handler Interface
// It defines how to link handlers and how to process the request.
type Handler interface {
Execute(*Data)
SetNext(Handler)
}
// Data represents the request context
type Data struct {
IsAuthenticated bool
IsValidated bool
IsCached bool
}
// 2. Base Handler: Authentication
type AuthHandler struct {
next Handler
}
func (h *AuthHandler) SetNext(next Handler) {
h.next = next
}
func (h *AuthHandler) Execute(d *Data) {
if d.IsAuthenticated {
fmt.Println("Auth: User already authenticated. Passing to next...")
if h.next != nil {
h.next.Execute(d)
}
return
}
fmt.Println("Auth: Authenticating user...")
d.IsAuthenticated = true
if h.next != nil {
h.next.Execute(d)
}
}
// 3. Concrete Handler: Validation
type ValidationHandler struct {
next Handler
}
func (h *ValidationHandler) SetNext(next Handler) {
h.next = next
}
func (h *ValidationHandler) Execute(d *Data) {
if d.IsValidated {
fmt.Println("Validation: Data already validated. Passing...")
if h.next != nil {
h.next.Execute(d)
}
return
}
fmt.Println("Validation: Validating request data...")
d.IsValidated = true
if h.next != nil {
h.next.Execute(d)
}
}
func main() {
// Initialize handlers
auth := &AuthHandler{}
validation := &ValidationHandler{}
// Create the chain: Auth -> Validation
auth.SetNext(validation)
// Execute the request
requestData := &Data{}
fmt.Println("Chain: Starting request processing...")
auth.Execute(requestData)
fmt.Printf("\nFinal Result: Auth: %v, Valid: %v\n",
requestData.IsAuthenticated, requestData.IsValidated)
}
C) Beyond the Syntax:
- Single Responsibility: Your authentication logic doesn’t know about validation logic. You can change one without touching the other.
- Dynamic Chains: You can change the order of the chain at runtime. Want to check the cache before authentication? Just swap the
SetNextcalls. - Open/Closed Principle: You can add a
LoggerHandleror aMetricsHandleranywhere in the chain without modifying existing handlers.
Relaying requests through a chain is vital for logic flow, but what about moving through your actual data? To navigate complex collections without getting lost in the implementation details, we reach for the Iterator Pattern.
7. Iterator Pattern:
In Go, we deal with slices and maps all the time. But what happens when your data is stored in a complex Binary Tree, a Graph, or a Linked List? If you force the client to understand how to traverse a tree (Left -> Root -> Right), you are leaking implementation details.
The Iterator Pattern provides a way to access elements of a collection without exposing its underlying representation. Think of Prayer Beads (Tespih) or a TV Remote: you don’t care how the beads are strung together or how the channels are stored in the TV’s memory; you just press the
A) The Architecture:
B) Go Implementation: The User Collection
Go doesn’t have a built-in Iterator interface like Java or C#, so we define our own. This is especially useful when your collection is fetched from an API in pages, and you want to hide that paging logic from the user.
package main
import "fmt"
// 1. The Item
type User struct {
Name string
}
// 2. The Iterator Interface
type Iterator interface {
HasNext() bool
Next() *User
}
// 3. The Collection Interface
type Collection interface {
CreateIterator() Iterator
}
// 4. Concrete Collection
type UserCollection struct {
users []*User
}
func (u *UserCollection) CreateIterator() Iterator {
return &UserIterator{
users: u.users,
}
}
// 5. Concrete Iterator
type UserIterator struct {
index int
users []*User
}
func (u *UserIterator) HasNext() bool {
return u.index < len(u.users)
}
func (u *UserIterator) Next() *User {
if u.HasNext() {
user := u.users[u.index]
u.index++
return user
}
return nil
}
func main() {
// Creating a collection
user1 := &User{Name: "Alice"}
user2 := &User{Name: "Bob"}
user3 := &User{Name: "Charlie"}
collection := &UserCollection{
users: []*User{user1, user2, user3},
}
// Traversing using the Iterator
iterator := collection.CreateIterator()
fmt.Println("Iterator: Walking through the users...")
for iterator.HasNext() {
user := iterator.Next()
fmt.Printf("User: %s\n", user.Name)
}
}
C) Architectural Mastery:
- Encapsulation: The client doesn’t know if
usersis a slice, a map, or a linked list. You could change the internal storage to a complex tree tomorrow, and thefor iterator.HasNext()loop wouldn't change a single character. - Uniform Interface: It allows you to write generic functions that can process any collection (Users, Products, Orders) as long as they provide an iterator.
- Safety: It prevents the client from accidentally modifying the collection while iterating, as they only have access to the
Next()element, not the underlying slice index.
Now that we know how to walk through every element in a collection, how do we perform specialized operations on them without cluttering their original code? Let’s invite a specialist in with the Visitor Pattern.
8. Visitor Pattern:
As a project grows, your data structs often become god objects. You start with a clean Project struct, but then you add ExportToPDF(), then CalculateTax(), then ValidateSecurity(). Soon, your data structures are bloated with logic that has nothing to do with data.
The Visitor Pattern separates the algorithms from the objects on which they operate. Think of a doctor’s visit: the patient (the Struct) stays still. A heart specialist (Visitor A) comes to check the heart, then a LungsSpecialist (Visitor B) comes to check the lungs. You don’t add DoHeartCheck() methods inside the Patient struct; you let external specialists "visit" the patient and perform their specific task.
A) The Architecture:
B) Go Implementation: The Project Exporter
In Go, the Visitor pattern is your ultimate weapon for defending the open/closed principle. It allows your data structs to stay clean and focused strictly on data. By defining an Accept method in our structs, we create a simple protocol that allows any external specialist whether they are exporting to PDF, calculating taxes, or auditing security to step in and work without us ever touching the original struct code again.
package main
import "fmt"
// 1. The Visitor Interface
// Defines what the "Specialist" can do to each type of data.
type Visitor interface {
VisitTask(*Task)
VisitEmployee(*Employee)
}
// 2. The Element Interface
// This allows our data structs to host a Visitor.
type Element interface {
Accept(Visitor)
}
// 3. Concrete Element: Task
type Task struct {
Name string
}
func (t *Task) Accept(v Visitor) {
v.VisitTask(t)
}
// 4. Concrete Element: Employee
type Employee struct {
Name string
}
func (e *Employee) Accept(v Visitor) {
v.VisitEmployee(e)
}
// 5. Concrete Visitor: PDF Exporter
type PDFExporter struct{}
func (p *PDFExporter) VisitTask(t *Task) {
fmt.Printf("PDF: Creating a beautiful page for Task [%s]\n", t.Name)
}
func (p *PDFExporter) VisitEmployee(e *Employee) {
fmt.Printf("PDF: Adding Employee [%s] to the project appendix\n", e.Name)
}
// 6. Concrete Visitor: Payroll Specialist
type PayrollSpecialist struct{}
func (ps *PayrollSpecialist) VisitTask(t *Task) {
// Tasks don't affect payroll, so we just skip.
}
func (ps *PayrollSpecialist) VisitEmployee(e *Employee) {
fmt.Printf("Payroll: Calculating salary and bonuses for %s...\n", e.Name)
}
func main() {
// Our data structures
projectElements := []Element{
&Task{Name: "Refactor Database"},
&Employee{Name: "Gopher Senior"},
}
// Operation 1: The CEO wants a PDF Export
pdfExporter := &PDFExporter{}
fmt.Println("--- Action: Generating PDF ---")
for _, el := range projectElements {
el.Accept(pdfExporter)
}
// Operation 2: HR wants to run Payroll
payroll := &PayrollSpecialist{}
fmt.Println("\n--- Action: Running Payroll ---")
for _, el := range projectElements {
el.Accept(payroll)
}
}
C) Why Seasoned Gophers Prefer This:
- Open/Closed Principle: You can add 10 new operations (JSON Export, Security Audit, Time Tracking) without changing a single line of code in the
TaskorEmployeestructs. - Separation of Concerns: Your data structs stay clean. They only contain the fields they need. All extraneous logic like exporting or calculating is moved to dedicated Visitor classes.
- Flexibility: It’s especially useful when dealing with complex object trees (like a file system or a project structure) where you need to perform different actions depending on the specific type of node you encounter.
Visitor allows us to add external logic to our structs, but sometimes we need a fixed internal skeleton for an algorithm while leaving the specific steps to the implementation. This brings us to the Template Pattern.
9. Template Pattern:
In engineering, we often have a fixed process that only varies in one or two steps. For example, every data miner follows the same workflow:
Open File -> Extract Data -> [Parse Specific Format] -> Close File.
If you write three different classes for CSV, PDF, and JSON, you’ll end up duplicating the Open and Close logic everywhere.
The Template Method defines the skeleton of an algorithm in a base class but allows subclasses to override specific steps without changing the algorithm’s overall structure. It’s like a Coloring Book : the lines are already drawn (the template), but you decide which colors to fill in (the implementation).
A) The Architecture:
B) Go Implementation: The Data Miner
Since Go doesn’t have abstract classes, we implement the Template Method by creating a master function or struct that accepts an interface for the variable steps.
package main
import "fmt"
// 1. The Interface for variable steps
type Miner interface {
Open()
Parse()
Close()
}
// 2. The "Template" Function
// This defines the fixed skeleton of the algorithm.
func RunMining(m Miner) {
fmt.Println("Template: Starting Workflow...")
m.Open()
m.Parse()
m.Close()
fmt.Println("Template: Workflow Completed.\n")
}
// 3. Concrete Implementation: CSV Miner
type CSVMiner struct{}
func (c *CSVMiner) Open() {
fmt.Println("CSV: Opening comma-separated file.")
}
func (c *CSVMiner) Parse() {
fmt.Println("CSV: Parsing rows and columns.")
}
func (c *CSVMiner) Close() {
fmt.Println("CSV: Closing file handle.")
}
// 4. Concrete Implementation: PDF Miner
type PDFMiner struct{}
func (p *PDFMiner) Open() {
fmt.Println("PDF: Opening binary document.")
}
func (p *PDFMiner) Parse() {
fmt.Println("PDF: Extracting text from coordinates.")
}
func (p *PDFMiner) Close() {
fmt.Println("PDF: Clearing memory buffers.")
}
func main() {
// Execute CSV Mining
csv := &CSVMiner{}
RunMining(csv)
// Execute PDF Mining
pdf := &PDFMiner{}
RunMining(pdf)
}
C) Future-Proofing Your Logic:
- DRY (Don’t Repeat Yourself): You write the high-level logic (error handling, logging, step order) only once in the template.
- Consistency: Every parser is guaranteed to close the file because the
Close()call is baked into the template, not left to the individual developer's memory. - Hook Support: You can add hooks (optional steps) in the template. For example, a
Validate()step that does nothing by default but can be overridden by specific miners if needed.
The Template Method ensures our workflows are consistent, but even the best workflows need a safety net. To capture a moment in time and undo any mistakes along the way, we need the save point of the Memento Pattern.
10. Memento Pattern:
Have you ever spent hours writing code only to realize your logic was fundamentally flawed, wishing you could just hit a global undo button? That is the essence of the Memento Pattern.
In complex applications, objects change state constantly. Sometimes, you need to save a snapshot of an object’s state so you can restore it later without exposing its private internals to the rest of the world. Think of it as a save point in a video game: you save your progress before a boss fight.
A)The Architecture:
B)Go Implementation: The Undo System
In Go, we keep the Memento struct simple and immutable. The Originator (the Editor) is the only one who knows how to use the Memento to travel back in time.
package main
import "fmt"
// 1. The Memento
// This is a "value object" that stores the state. It should be immutable.
type Memento struct {
state string
}
func (m *Memento) GetSavedState() string {
return m.state
}
// 2. The Originator
// This is the object whose state we want to save.
type Editor struct {
content string
}
func (e *Editor) Write(text string) {
e.content += text
}
func (e *Editor) Save() *Memento {
fmt.Printf("Editor: Saving snapshot -> \"%s\"\n", e.content)
return &Memento{state: e.content}
}
func (e *Editor) Restore(m *Memento) {
e.content = m.GetSavedState()
fmt.Printf("Editor: State restored to -> \"%s\"\n", e.content)
}
// 3. The Caretaker
// Manages the history of mementos.
type History struct {
mementos []*Memento
}
func (h *History) Push(m *Memento) {
h.mementos = append(h.mementos, m)
}
func (h *History) Pop() *Memento {
if len(h.mementos) == 0 {
return nil
}
lastIndex := len(h.mementos) - 1
m := h.mementos[lastIndex]
h.mementos = h.mementos[:lastIndex]
return m
}
func main() {
editor := &Editor{}
history := &History{}
// Step 1: User writes something and saves
editor.Write("Hello ")
history.Push(editor.Save())
// Step 2: User writes more and saves
editor.Write("World!")
history.Push(editor.Save())
// Step 3: User makes a mistake
editor.Write(" This is a mess.")
fmt.Printf("Current Content: %s\n", editor.content)
// Step 4: Undo
fmt.Println("Action: User clicks Undo.")
editor.Restore(history.Pop()) // Back to "Hello World!"
// Step 5: Undo again
editor.Restore(history.Pop()) // Back to "Hello "
}
C) The Maintainability Factor:
- Encapsulation Protection: The
History(Caretaker) stores the state but has no idea what’s inside it. It just holds a black box. - Memory Management: A Senior architect knows that storing thousands of mementos can kill RAM. You would implement a limited stack (e.g., only the last 20 actions) or use diff-based snapshots for large data.
- Separation of Concerns: The
Editorfocuses on editing. TheHistoryfocuses on storage. Neither leaks logic into the other.
Behavioral Cheat Sheet
We’ve choreographed the dance. We’ve turned our rigid machine into an ecosystem. To help you choose the right partner for your next Go project, here is the ultimate behavioral summary:
You’ve finished the trilogy. You now have the Creational tools to build your world, the Structural glue to hold it together, and the Behavioral intelligence to make it move.
Go is a language of simplicity. These patterns are not meant to make your code fancy, they are meant to make it resilient. Use them wisely, don’t over-engineer, and always remember: Code is for humans to read, and only incidentally for machines to execute.
Keep building, keep learning, and Keep calm and recover() from your panic ;))
See you in the next article 🚀



























Top comments (0)