DEV Community

Cem AKAN
Cem AKAN

Posted on

Golang Design Patterns: Creational Patterns

captionless image

Hello, Go enthusiasts. Welcome to the first installment of our deep dive into Golang Design Patterns 🎉

In this part, we’ll be mastering the Creational Patterns. These patterns are the fundamental building blocks for sophisticated software architecture, focusing on making your object creation process flexible, reusable, and decoupled from your core business logic.

We’ve explored five powerful techniques:

  • Singleton Pattern
  • Factory Pattern
  • Abstract Factory Pattern
  • Builder Pattern
  • Prototype Pattern

Let’s start :)

First of all, what is the design patterns?

Especially, we can see this term after object-oriented programming (OOP) entered our lives. After the long development years, programmers saw they repeated using the same templates. And these repeated patterns showed us that if we solved a problem in one of them, we could solve the other repeated ones. So, programmers created an approach which includes common problems and solution methods for repeated structures and named it design patterns.

We can categorize design patterns in three subtitles: Creational Patterns, Structural Patterns and Behavioral Patterns.

1- Creational Patterns:

captionless image

We use creational patterns for manage object creation process and optimize it.

Singleton Pattern:

captionless image

Our first creational pattern is singelton pattern. We use this method for creation only one object from the struct in our project. In singelton method, if we try to re-create a same type object after the first create, our program must detects it and returns first created object to us.

We provide an example of this method to make it more understandable. So let’s look at the diagram and code for the example below.

Diagram:

captionless image

We define a struct which name is “Singelton”. It has a string variable. And we create a variable from it and was named it “instance”. After that, we define a function for assignment to instance variable. With this function, we also limited the create with one and provide to return first created object for later creates.

Code:

package main
import (
 "fmt"
 "sync"
)
// Singleton struct represents the Singleton design pattern.
// It has a single instance that is shared across the application.
type Singleton struct {
 value string // value holds the state or data for the Singleton instance
}
// instance holds the single instance of Singleton. It is initialized as nil.
var instance *Singleton
// once ensures that the Singleton instance is created only once.
// sync.Once is a type from the sync package that allows a function to be executed only once.
var once sync.Once
// GetInstance returns the single instance of Singleton. 
// It creates the instance if it doesn't already exist.
func GetInstance() *Singleton {
 // sync.Once.Do ensures that the provided function is executed only once.
 // This guarantees that only one instance of Singleton is created.
 once.Do(func() {
  instance = &Singleton{
   value: "Singleton Instance", // Initialize the Singleton instance with a default value
  }
 })
 return instance
}
// main function demonstrates the usage of the Singleton pattern.
func main() {
 // Retrieve the Singleton instance using GetInstance.
 singleton1 := GetInstance()
 singleton2 := GetInstance()
 // Print the value of both instances.
 // Since Singleton is a singleton, both instances should have the same value.
 fmt.Println(singleton1.value) // Output: Singleton Instance
 fmt.Println(singleton2.value) // Output: Singleton Instance
 // Check if both instances are the same.
 // This should return true because Singleton ensures only one instance exists.
 if singleton1 == singleton2 {
  fmt.Println("Both instances are the same.")
 } else {
  fmt.Println("Instances are different.")
 }
}
Enter fullscreen mode Exit fullscreen mode

Factory Pattern:

captionless image

Factory pattern (virtual constructor) provides an interface for creating objects, but let superclasses decide which type of objects that will be created.

Commonly, we use this method to define functions with the same purpose for different structs and avoid using different names for these functions by collecting them into a common interface. This approach helps us manage user functions more clearly in our service.

Okay, let’s make an example to understand it more clearly.

Imagine that you’re creating a geometry calculator application. It can calculate square’s and circle’s area and perimeter with their piece’s length.

But these calculator functions should have same name for clear user controls. So we give them “area” and “perim” names.

Now, let’s look at the diagram below and examine the mechanism of our code. If you don’t fully understand, don’t worry; everything will be explained at the end of the diagram.

Diagram:

captionless image

Firstly, we define a two struct scheme: “circle” and “square” which has special parameters. And we define functions: “area” and “perim” which return’s type float64, and we link them with “areaAndPerim” interface to reach them above struct with their common names. After that we create a super function which name is “GeometryFactory”. It creates struct based on the user’s choice and returns “areaAndPerim” interface.

In the final, when users import our service, they can only see the super function and interface based common functions.

Now that we understand the concept, we can look at its implementation in code.

Code:

package main
// we import the math package to use the math.Pi constant
import "math"
type (
 // we define a circle struct with a radius field of type float64
 circle struct {
  radius float64
 }
 // we define a square struct with a side field of type float64
 square struct {
  side float64
 }
 // we define an interface with two methods area and perim that return float64 values
 areaAndPerim interface {
  area() float64
  perim() float64
 }
)
// we define the area method for the circle struct
func (c circle) area() float64 {
 return 2 * math.Pi * c.radius
}
// we define the perim method for the circle struct
func (c circle) perim() float64 {
 return math.Pi * c.radius * c.radius
}
// we define the area method for the square struct
func (s square) area() float64 {
 return s.side * s.side
}
// we define the perim method for the square struct
func (s square) perim() float64 {
 return 4 * s.side
}
// we define a GeometryFactory function that returns an areaAndPerim interface
func GeometryFactory(typeName string, pieceLength float64) areaAndPerim {
 // if the typeName is circle, we return a circle struct with the radius equal to pieceLength
 if typeName == "circle" {
  return circle{radius: pieceLength}
  // if the typeName is square, we return a square struct with the side equal to pieceLength
 } else if typeName == "square" {
  return square{side: pieceLength}
  // if the typeName is neither circle nor square, we return nil
 } else {
  return nil
 }
}
func main() {
 // we create a circle and a square with the GeometryFactory function
 c := GeometryFactory("circle", 5)
 s := GeometryFactory("square", 5)
 // we print the area and perim of the circle and the square
 println(c.area())
 println(c.perim())
 println(s.area())
 println(s.perim())
}
Enter fullscreen mode Exit fullscreen mode

Abstract Factory Pattern*:*

captionless image

We use abstract factory method, when we work on multiple factories. We make a main factory maker function to reach them. And after that we can use all features of created factories.

Then, without losing any time, let’s look at an example diagram and code for this pattern.

Diagram:

captionless image

In this example, we have two factory complexes. We want to manage them using a single main factory creator function, which we call “AbstractFactory” in the diagram. Using this main function, we can create individual factories and utilize their specific substructures and functions through the associated interfaces.

However, we need to ensure that the abstract factory’s return function (import variables) and interface{} are correctly configured. Our factories must have the same type and number of variables, and their prototype forms should be defined in the abstract factory. To provide a common prototype, we use interface{} for the return type.

After creating a factory, to use it, we should append the specific interface name of that factory with parentheses.

Code:

package main
import "fmt"
type (
 // Structs definition
 A struct {
  text string
 }
 B struct {
  text string
 }
 C struct {
  text string
 }
 D struct {
  text string
 }
 // Interfaces definitions
 InterfaceA interface {
  Func1() string
  Func2() string
 }
 InterfaceB interface {
  Func3() string
  Func4() string
 }
 // Abstract Factory definition for simple returns
 Factory func(string, string) interface{}
)
//function for interfaceA
// functions for struct A
func (a *A) Func1() string {
 return a.text + " Func1"
}
func (a *A) Func2() string {
 return a.text + " Func2"
}
// functions for struct B
func (b *B) Func1() string {
 return b.text + " Func1"
}
func (b *B) Func2() string {
 return b.text + " Func2"
}
//function for interfaceB
// functions for struct C
func (c *C) Func3() string {
 return c.text + " Func3"
}
func (c *C) Func4() string {
 return c.text + " Func4"
}
// functions for struct D
func (d *D) Func3() string {
 return d.text + " Func3"
}
func (d *D) Func4() string {
 return d.text + " Func4"
}
// Factory functions definitions. They create a struct based on typeName and return the interface{} type
// Factory function for interfaceA
func FirstFactory(typeName, text string) interface{} {
 switch typeName {
 case "A":
  return &A{text}
 case "B":
  return &B{text}
 }
 return nil
}
// Factory function for interfaceB
func SecondFactory(typeName, text string) interface{} {
 switch typeName {
 case "C":
  return &C{text}
 case "D":
  return &D{text}
 }
 return nil
}
// Abstract Factory for first and second factories. It returns a factory function based on the factoryType, with these functions we can create structs based on the typeName
func CreateFactory(factoryType string) Factory {
 switch factoryType {
 case "FirstFactory":
  return FirstFactory
 case "SecondFactory":
  return SecondFactory
 }
 return nil
}
// Main function,
func main() {
 // Setup the first factory
 firstFactory := CreateFactory("FirstFactory")
 // Create a struct A
 a := firstFactory("A", "Hello").(InterfaceA)
 // Setup the second factory
 secondFactory := CreateFactory("SecondFactory")
 // Create a struct C
 c := secondFactory("C", "World").(InterfaceB)
 // Call the functions for the structs
 fmt.Println(a.Func1())
 fmt.Println(c.Func3())
}
/*
Output:
Hello Func1
World Func3
*/
Enter fullscreen mode Exit fullscreen mode

Builder Pattern:

captionless image

The Builder pattern is used for step-by-step construction of complex objects that have numerous optional components. It is especially useful when a product has multiple configurations.

When it was used, we can use predefined functions in addition to step-by-step functions that provide creating custom objects.

Okay, let’s look at an example diagram and code for this pattern.

Graph:

captionless image

We define a “ConcretePizzaBuilder” struct that include “Pizza” struct to create methods. With this we can links configuration functions with each others.

In configuration functions we return the PizzaBuilder interface for continuing to change same object in the configuration function chain.

In the final, we end our function chain with the “Buid” function that returns “Pizza” object in the ConcretePizzaBuilder struct.

Also, we can use pre-defined functions to create objects. For using these functions, we PizzaDirector struct.

Code:

package main
import "fmt"
type (
 // Pizza struct represents a pizza
 Pizza struct {
  size      string
  cheese    bool
  pepperoni bool
 }
 // PizzaBuilder is an interface for creating a pizza
 PizzaBuilder interface {
  SetSize(size string) PizzaBuilder
  AddPepperoni() PizzaBuilder
  AddCheese() PizzaBuilder
  Build() Pizza
 }
 // ConcertePizzaBuilder is a struct that implements PizzaBuilder
 ConcertePizzaBuilder struct {
  pizza Pizza
 }
 // PizzaDirector is a struct that directs the creation of a pizza
 PizzaDirector struct{}
)
// SetSize sets the size of the pizza
func (pb *ConcertePizzaBuilder) SetSize(size string) PizzaBuilder {
 pb.pizza.size = size
 // Set default values
 pb.pizza.cheese = false
 pb.pizza.pepperoni = false
 return pb
}
// AddPepperoni adds pepperoni to the pizza
func (pb *ConcertePizzaBuilder) AddPepperoni() PizzaBuilder {
 pb.pizza.pepperoni = true
 return pb
}
// AddCheese adds cheese to the pizza
func (pb *ConcertePizzaBuilder) AddCheese() PizzaBuilder {
 pb.pizza.cheese = true
 return pb
}
// Build builds the pizza
func (pb *ConcertePizzaBuilder) Build() Pizza {
 return pb.pizza
}
// Predefined pizza creator functions
// CreatePepperoniPizza creates a pepperoni pizza of a given size
func (pd *PizzaDirector) CreatePepperoniPizza(size string, builder PizzaBuilder) Pizza {
 return builder.SetSize(size).AddPepperoni().AddCheese().Build()
}
func main() {
 // initialize the builder and director objects
 builder := &ConcertePizzaBuilder{}
 director := &PizzaDirector{}
 // Create a large pizza with pepperoni and cheese
 pizza := director.CreatePepperoniPizza("large", builder)
 fmt.Println("Predefined pepperoni pizza:", "size:", pizza.size, "cheese:", pizza.cheese, "pepperoni:", pizza.pepperoni)
 // Custom pizza creation
 // Create a custom pizza with medium size with cheese
 customPizza := builder.SetSize("medium").AddCheese().Build()
 fmt.Println("Custom pizza:", "size:", customPizza.size, "cheese:", customPizza.cheese, "pepperoni:", customPizza.pepperoni)
}
Enter fullscreen mode Exit fullscreen mode

Prototype Pattern:

captionless image

The Prototype pattern allows you to copy existing objects without making your code dependent on related objects.

The idea is to create new objects by copying an existing prototype rather than building them from scratch.

This pattern is particularly useful when object creation is time-consuming and costly.

Okay, let’s look at an example diagram and code.

Diagram:

captionless image

Firstly, we define a “Cloner” interface which contains “Clone” method function which associate with “Rectangle” and “Circle” structs. After the defining related structs, we start to define the “Clone” functions.

The function, it use created objects parameters for cloning to new one. After that, it returns “Cloner” interface to back because return object’s type can be change.

Finally, in the end of “Clone” function call, we should add object’s struct name between brackets to define their type.

Code:

package main
import "fmt"
type (
 // define Cloner interface
 Cloner interface {
  Clone() Cloner
 }
 // define Rectangle struct with Width & Height parameters
 Rectangle struct {
  Width, Height int
 }
 //define Circle struct with Radius parameter
 Circle struct {
  Radius int
 }
)
// cloner function for Circle
func (c *Circle) Clone() Cloner {
 newCircle := &Circle{
  Radius: c.Radius,
 }
 return newCircle
}
// cloner function for Rectangle
func (r *Rectangle) Clone() Cloner {
 newRectangle := &Rectangle{
  Width:  r.Width,
  Height: r.Height,
 }
 return newRectangle
}
func main() {
 // create new Circle object
 circle := Circle{
  Radius: 10,
 }
 // clone Circle object
 newCircle := circle.Clone().(*Circle)
 // create new Rectangle object
 rectangle := Rectangle{
  Width:  10,
  Height: 20,
 }
 // clone Rectangle object
 newRectangle := rectangle.Clone().(*Rectangle)
 // print newCircle object
 fmt.Println(newCircle)
 // print newRectangle object
 fmt.Println(newRectangle)
}
Enter fullscreen mode Exit fullscreen mode

We’ve laid the groundwork for flexible and controlled object creation in Go by covering the Singleton, Factory, Abstract Factory, Builder, and Prototype patterns. These are your tools for writing decoupled, scalable, and maintainable code.

Next time, we shift gears to Structural Patterns, learning how to compose Go’s structs and interfaces into robust, beautiful architectures.

Don’t miss out. Stay tuned. Byee :D

captionless image

Top comments (0)