Design Patterns in Go

Bora Kaşmer
The Startup
Published in
13 min readFeb 26, 2021
Benjamin Lee. (Fushimi Inari-Taisha Shrine)

Today we will talk about how to use design patterns in Go. Is it really necessary? Is Go suitable for design patterns? Let’s implement some Design Patterns.

“A design that doesn’t take change into account risks major redesign in the future.”
― Erich Gamma, Design Patterns: Elements of Reusable Object-Oriented Software

Strategy Design Pattern:

The Strategy pattern basically allows an object to select and execute at runtime without knowing how to implement the business logic it wants, instead of using different types of algorithms that can be used to perform each operation.
I use this pattern in Go very often. And I think Golang is so suitable for this pattern.
In this example, we will create a package, which could connect different types of DB. And in the future, we can add a new type of DB Connection easily.

As seen below, I created “IDBConnection” interface and common “Connect()” method for all inherited structs.

IDBConnection Interface

As seen below, I created the SqlConnection struct, and for implementing to IDBconnect interface, I wrote the “Connection()” function.

Sql Connection

As seen below, I did the same thing for OracleConnection.

Oracle Connection

As seen below, the DBConnection struct is used for calling Connect() method of the struct, which is implemented from the IDBConnection interface.

As seen below, we created SQL and Oracle Connection struct. Both of them set the “db” property of the DBConnection struct. And DBConnection struct call same method “DBConnect()” for Oracle and SQL DB Connection.

Example main() method

This is the result of the above code:

Result

Strategy Design Pattern Full Code:

main.go:

package main

import (
"fmt"
type IDBconnection interface {
Connect()
})
type SqlConnection struct {
connectionString string
}

func (con SqlConnection) Connect() {
fmt.Println(("Sql " + con.connectionString))
}

type OracleConnection struct {
connectionString string
}

func (con OracleConnection) Connect() {
fmt.Println("Oracle " + con.connectionString)
}

type DBConnection struct {
db IDBconnection
}

func (con DBConnection) DBConnect() {
con.db.Connect()
}

func main() {
sqlConnection := SqlConnection{connectionString: "Connection is connected"}
con := DBConnection{db: sqlConnection}
con.DBConnect()

orcConnection := OracleConnection{connectionString: "Connection is connected"}
con2:=DBConnection{db:orcConnection}
con2.DBConnect()
}

“Everyone thinks of changing the world, but no one thinks of changing himself.”
Leo Tolstoy

Observer Design Pattern:

The principle of the pattern is to convey the changes that may occur in the objects connected by one-to-many relationships to the others. The main purpose is to notify the relational groups about the change.

DesignPatterns/Observer: This is the observer interface and has two Update() and GetID() functions.

customer/observer.go:

package customer

type Observer interface {
Update(string)
GetID()string
}

DesignPatterns/Customer:

We will implement all functions of the Observer interface in the Customer struct.

  • “Id”: property is the name of the customer.
  • Update()”: function takes product name as a parameter and notifies the customer’s changed product with email.
  • “GetID()”: Function returns to the customer’s Id, in other word’s customer’s email.

customer/customer.go:

package customer

import "fmt"

type Customer struct{
Id string
}

func(c *Customer) Update(productName string){
fmt.Printf("%s müşterisine %s ürünü için email gönderiliyor.\n", c.Id, productName)
}

func(c *Customer) GetID() string{
return c.Id
}

DesignPatterns/ProductManage: This is the product interface. We have to implement all the below functions into the product struct.

customer/productManage.go:

package customer

type ProductManage interface {
Register(Observer Observer)
RegisterList(Observer []Observer)
Unregister(Observer Observer) []Observer
NotifyAll()
}

DesignPatterns/Product: This is the product struct. All customers monitor this struct.

product struct
  • observerList: List of notified customers.
  • name: Product name
  • inStock: This is the state of product stock info.
func NewProduct(_name string) *product {
  • NewProduct(): This function returns a new product with a name by referance.”return &product{name: _name}”. This is the kind of constructor of the Product.
func (p *product) NotifyAll() {
  • NotifyAll(): This function notifies all related clients in the observer list when the product Update. We’ll walk through the observerList step by step and call the Customer Update() function in “for _, observer := range p.oberverList
  • Register(): We will add a new customer to the product’s observerList.
  • RegisterList(): We will add more than one customer to the product’s observerList.
  • RemoveFromList(): We will remove the specific customer from the customerList by its Id. “if removeObserver.GetID() == observer.GetID()” if we will find the customer from the list by its Id. “return append(observerList[:index], observerList[index+1:]…)” We will slice the observerList from beginning to finding Customer Index and append beginning from one more finding Customer Index to the end. So we will remove the selected customer from the list by its Id.
RemoveFromList
  • Unregister(): We will call RemoveFromList() function in this function and remove the selected Customer from the ObserverList.

customer/product.go:

package customer

import (
"fmt"
"strings"
)

type product struct {
oberverList []Observer
name string
inStock bool
}

func NewProduct(_name string) *product {
return &product{name: _name}
}

func (p *product) NotifyAll() {
for _, observer := range p.oberverList {
observer.Update(p.name)
}
}

func (p *product) UpdateAvalabilitiy() {
fmt.Printf("Product %s artık stoklardadır\n", p.name)
fmt.Printf(strings.Repeat("-", 100))
fmt.Printf("\n")
p.inStock = true
p.NotifyAll()
}

func (p *product) Register(o Observer) {
p.oberverList = append(p.oberverList, o)
}

func (p *product) RegisterList(olist []Observer) {
for _, o := range olist {
p.oberverList = append(p.oberverList, o)
}
}

func RemoveFromList(observerList []Observer, removeObserver Observer) []Observer {
for index, observer := range observerList {
if removeObserver.GetID() == observer.GetID(){
return append(observerList[:index], observerList[index+1:]...)
}
}
return observerList
}

func (p *product) Unregister(o Observer) []Observer {
p.oberverList = RemoveFromList(p.oberverList, o)
return p.oberverList
}

DesignPatterns/main: This the test of the observer function. “NewProduct()” function called from customer struct and return new PS5 product. We added two customers to this product’s observerList by calling “RegisterList()” function. Finally we called UpdateAvalabilitty() product’s function. And NotifyAll() function called in this function. And the last state of the PS5 product notified to all customers, which is in the observerList of products.

main.go:

func main() {
ps5 := cus.NewProduct("Sony Playstation 5")
observer1 := &cus.Customer{Id: "bora@borakasmer.com"}
observer2 := &cus.Customer{Id: "martin@martinfowler.com"}
ps5.RegisterList([]cus.Observer{observer1, observer2})
ps5.UpdateAvalabilitiy()
}

This is the result of the above code:

Prototype Design Pattern:

At the time we develop software, there may be objects that are costly. Cost means runtime and memory usage. So these objects’ runtime is long and keeps a large place in the memory. A prototype template has been considered in order to use these objects more effectively and efficiently. This template aims to avoid this problem by cloning an existing object. Therefore, we do not go into the trouble of constantly creating new objects. In other words, an object that is thought to be used later is wanted to be a prototype and others to benefit from it. The prototype object is copied, and now it is processed on this copy. Thus, the original object is also preserved.

In this example, we will create different types of fighters for a video game.

We create a fighter interface. We will use “FighterType” as an Enum. We keep fighter type on this variable.

Fighter” is the core Interface. We will create all fighter structs from this Fighter interface. It has three functions:

  • GetId()”: We will get the unique Id of the Fighter struct.
  • Fight()”: We will print Fighters Hit-Point on the console.
  • Clone()”: We will get a deep copy of the Fighter struct with this function.

fighter/Fighter.go(1):

package fighter

import "fmt"

type FighterType int

const (
SoldierType FighterType = 1
ElfType = 2
DwarfType = 3
)

type Fighter interface {
GetId() FighterType
Fight()
Clone() Fighter
}

Soldier:

We will create a new character from the Fighter interface. It has four properties.

  • Id: Unique FighterType Enum Id.
  • GunHit: Fighters’ Hit-Point.
  • Life:It’s Soldier’s life power.
  • Mana: It is Soldier’s stamina.

NewSoldier()”: We call this function from the “Clone()” function. We will create a new Soldier struct by using this function. “SolderType, gunHit, life, and mana” are parameters of this function. This the constructor of Soldier.

GetId()”: This function returns us FighterType of “Soldier” struct. Actually, it is 1.

Clone()”: This function is used for getting DeepCopy of “Soldier” Struct, which is inherited from the Fighter interface.

Fight()”: This function is showing the HitPoint of the Soldier Struct.

fighter/Fighter.go(2):

type Soldier struct {
Id FighterType
GunHit int
Life int
Mana int
}

func NewSoldier(gunHit, life, mana int) Soldier {
soldier := Soldier{SoldierType, gunHit, life, mana}
return soldier
}

func (s Soldier) GetId() FighterType {
return s.Id
}

func (s Soldier) Clone() Fighter {
return NewSoldier(s.GunHit, s.Life, s.Mana)
}

func (s Soldier) Fight() {
fmt.Println("Soldier Gun Hit-Points:", s.GunHit)
}

Elf:

We will create a new character from the Fighter interface like Soldier struct. It has four properties too.

  • Id: Unique FighterType Enum Id. It is 2
  • ArrowHit: Elf’s Hit-Point.
  • Life: It’s Elf’s life power.
  • Mana: It is Elf’s stamina.

NewElf(), GetId(), Clone() and Fight()” are the same functions like Soldier struct.

fighter/Elf.go:

package fighter

import "fmt"

type Elf struct {
Id FighterType
ArrowHit int
Life int
Mana int
}

func NewElf(arrowHit, life, mana int) Elf {
return Elf{ElfType, arrowHit, life, mana}
}

func (e Elf) GetId() FighterType {
return e.Id
}

func (e Elf) Clone() Fighter {
return NewElf(e.ArrowHit, e.Life, e.Mana)
}

func (e Elf) Fight() {
fmt.Println("Elf Arrow Hit-Point:", e.ArrowHit)
}

Dwarf:

We will create a new character from the Fighter interface like Soldier struct. All the functions and Properties are the same as Soldier and Elf Structs.

fighter/Dwarf.go:

package fighter
import "fmt"

type Dwarf struct {
Id FighterType
AxePoint int
Life int
Mana int
}

func NewDwarf(arrowHit, life, mana int) Dwarf {
return Dwarf{DwarfType, arrowHit, life, mana}
}

func (e Dwarf) GetId() FighterType {
return e.Id
}

func (e Dwarf) Clone() Fighter {
return NewDwarf(e.AxePoint, e.Life, e.Mana)
}

func (e Dwarf) Fight() {
fmt.Println("Dwarf Axe Hit-Point:", e.AxePoint)
}

DesignPatterns/main: Firstly, we will create a couple of prototype Fighters for the model characters. After all, other characters will Clone() from these characters. So Runtime and Memory Usage will be less by using this Prototype Pattern.

dataHistoryList: This “[int]Fighter” is Hash Table. Key integer and value Fighter interface. So we can put Soldier, Elf, and Dwarf characters are in it. We put soldier and dwarf Fighters in this dataHistoryList.

func loadCache():

Main.go(1):

var dataHistoryList map[int]fig.Fighter

func loadCache() {
soldier := fig.NewSoldier(30, 20, 5)
dwarf := fig.NewDwarf(50, 40, 15)
dataHistoryList = make(map[int]fig.Fighter)
dataHistoryList[int(soldier.GetId())] = soldier
dataHistoryList[int(dwarf.GetId())] = dwarf
}

func main(): We will call loadCache() for filling the dataHistoryList.

  • fighter := dataHistoryList[int(fig.SoldierType)]”: We try to get Soldier from dataHistoryList
  • f, ok := fighter.(fig.Soldier)”: We try to cast fighter Interface to Soldier struct.
  • if ok { newFighter := f.Clone().(fig.Soldier)”: If it is ok, we will Clone() it from the Soldier struct. And we will change the GunHit property to 45.

Main.go(2):

func main() {
loadCache()
fighter := dataHistoryList[int(fig.SoldierType)]
f, ok := fighter.(fig.Soldier)
if ok {
newFighter := f.Clone().(fig.Soldier)
newFighter.GunHit = 45
newFighter.Fight()
}

We try to get Elf from the dataHistoryList. If there is an Elf struct in this list, we can Clone() it, but if we don’t, we will create a new Elf with the “NewElf()” function. And add it to the dataHistoryList. And finally, we will call Fight() function of Elf struct.

Main.go(3):

Second Elf fighter is certainly in the dataHistory, so we find and Clone() it. And set the new “ArrowHit” property to 35. And finally, call Fight() function of second Elf struct.

Main.go(4):

We will create a dwarf Fighter as the last fighter character. We try to get dwarf from the dataHistoryList. If there is a dwarf struct in this list, we can Clone() it, but if we don’t, we will create a new dwarf with the “NewDwarf()” function. And add it to the dataHistoryList. And finally,we will call Fight() function of the Dwarf struct. But if you remember, we added dwarf fighter in loadCache() function before. So we will find dwarf Fighter and Clone() it.

Main.go(5):

main.go (Full):

func main() {
loadCache()
fighter := dataHistoryList[int(fig.SoldierType)]
f, ok := fighter.(fig.Soldier)
if ok {
newFighter := f.Clone().(fig.Soldier)
newFighter.GunHit = 45
newFighter.Fight()
}

elf := dataHistoryList[int(fig.ElfType)]
e, ok := elf.(fig.Elf)
if ok {
newElf := e.Clone()
newElf.Fight()
} else {
elf := fig.NewElf(15, 30, 3)
dataHistoryList[int(elf.GetId())] = elf
elf.Fight()
}
elf2 := dataHistoryList[int(fig.ElfType)]
e2, ok := elf2.(fig.Elf)
if ok {
newElf2 := e2.Clone().(fig.Elf)
newElf2.ArrowHit = 35
newElf2.Fight()
}

dwarf := dataHistoryList[int(fig.DwarfType)]
d, ok := dwarf.(fig.Dwarf)
if ok {
newDwarf := d.Clone()
newDwarf.Fight()
} else {
dwarf := fig.NewDwarf(50, 40, 15)
dataHistoryList[int(fig.DwarfType)] = dwarf
dwarf.Fight()
}
}

This is the result of the above code:

“Life is not reversible like video games, so think twice do once.” ― Bora Kaşmer

Memento Design Pattern :

Essentially, it is a pattern designed to keep the previous state (s) of an object and retrieve it when desired. We can also think of objects as gaining the ability to undo or redo their internal state (Initial State). In this mold, there is a copy of the object whose status is to be preserved, exactly or at least keeping the areas (features) that are desired to be kept (Memento).

For this example, as seen below, we will create motorcycle sales data. We will create MotorType as an Enum, and we will keep the State of ServiceData structure for undo() and redo() functions.

motorcycle/motorcycle.go(1):

As seen below, we will create a reference int counter for keeping the index of ServiceData in the HashTable(“var counter *int”). Our HashTable is dataHistoryList. Integer key and ServiceData value(“var dataHistoryList map[int]ServiceData”). “UpdateDataChange()” function is added new state of “ServiceData” to the “dataHistoryList” with counter index. If dataHistoryList is nill we will create new one with “make()” function. For every ServiceData, we will increase the counter. And set this counter to the dataHistoryList as the index. With this, we keep all states of ServiceData in this dataHistoyList hash table. So later, we can move back and forward on this dataHistoryList by changing this counter index.

motorcycle/motorcycle.go(2):

As seen below, we can return the previous state of ServiceData on dataHistoryList by using “Undo()” function. Only we have to subtract one from the counter index, and if it is bigger than zero, take the ServiceData from dataHistoryList by using this counter index.

Image URL: y7eeund41gfpzdlbdvvi.gif

We can return the next state of ServiceData on dataHistoryList by using the “Redo()” function. We have to increase one from the counter index. If it is not bigger than the length of the data history HashTable, takes the next ServiceData from dataHistoryList by using this counter index.

motorcycle/motorcycle.go(3):

motorcycle/motorcycle(Full).go:

package motorcycle

import "time"

type MotorType int

const (
Kawasaki MotorType = iota
Honda
Suzuki
Ducati
)

type ServiceData struct {
Id int
PersonalName string
Model MotorType
Price float32
Date time.Time
}

var counter *int
var dataHistoryList map[int]ServiceData

func UpdateDataChange(model *ServiceData) {
if dataHistoryList == nil {
dataHistoryList = make(map[int]ServiceData)
counter = new(int)
}
*counter++
model.Id = *counter
dataHistoryList[*counter] = *model
}

func Undo() ServiceData {
index := *counter - 1
if index >= 1 {
*counter = index
return dataHistoryList[index]
}
return dataHistoryList[*counter]
}

func Redo() ServiceData {
index := *counter + 1
if index <= len(dataHistoryList) {
*counter = index
return dataHistoryList[*counter]
}
return dataHistoryList[*counter]
}

main(): As seen below, we will create three ServiceData states and add them to the “dataHistoryList.

  • And later, when we call the “Undo()” function, we will get the previous state of ServiceData. Actually, we will move from “Martin Fowler” to => “Kent Back.”
  • When we call the “Undo()” function a second time, we will get the previous state of ServiceData, so we will move from “Kent Back” to => “Bora Kasmer.”
  • When we call the “Redo()” function, we will move to the next state of ServiceData. Actually, we will move from “Bora Kasmer” to => “Kent Back.”
  • When we call the “Redo()” function a second time, we will move to the next state of ServiceData. Actually, we will move from “Kent Back” to => “Martin Fowler.”. So we will come the current position.

So with this Memento Design Pattern, we can move next and previous state of ServiceData by keeping all changes in a HashTable.

main.go:

func main() {
mainData := motor.ServiceData{PersonalName: "Bora Kasmer", Model: motor.Honda, Price: 105000, Date: time.Now()}
motor.UpdateDataChange(&mainData)
fmt.Println(mainData)

mainData = motor.ServiceData{PersonalName: "Kent Back", Model: motor.Kawasaki, Price: 170000, Date: time.Now()}
motor.UpdateDataChange(&mainData)
fmt.Println(mainData)

mainData = motor.ServiceData{PersonalName: "Martin Fowler", Model: motor.Ducati, Price: 330000, Date: time.Now()}
motor.UpdateDataChange(&mainData)
fmt.Println(mainData)

mainData = motor.Undo()
fmt.Println("1 Geri Git: ", mainData)
fmt.Println(strings.Repeat("-", 100))

mainData = motor.Undo()
fmt.Println("1 Geri Git: ", mainData)
fmt.Println(strings.Repeat("-", 100))

mainData = motor.Redo()
fmt.Println("1 İleri Git: ", mainData)
fmt.Println(strings.Repeat("-", 100))

mainData = motor.Redo()
fmt.Println("1 İleri Git: ", mainData)
}

This is the result of the above code:

“The number one benefit of information technology is that it empowers people to do what they want to do. It lets people be creative. It lets people be productive. It lets people learn things they didn’t think they could learn before, and so in a sense it is all about potential.”

― Steve Ballmer

Conclusion:

In this article, we tried to understand how to use some Design Patterns on Go Lang. Go Lang’s main purpose is simplicity and speed. But if we can use the interface on Go, we can benefit from some Design Patterns’ blessings. We don’t have to resolve a problem again that has been solved before. So using Design Patterns in Go is up to you. But if I were you, I gave a chance to Design Patterns in Go.

I hope this article helps you to understand some Design Patterns and using them in Go.

“If you have read so far, first of all, thank you for your patience and support. I welcome all of you to my blog for more!”

Source Code: https://github.com/borakasmer/GoDesignPatterns

Source:

--

--

Bora Kaşmer
The Startup

I have been coding since 1993. I am computer and civil engineer. Microsoft MVP. Software Architect(Cyber Security). https://www.linkedin.com/in/borakasmer/