Go Specific Design Patterns
Design patterns are reusable solutions to common software design problems. On this page, we will discuss of design patterns that rely on Go specific features to make the application productive.
Constructor Pattern
A constructor in Go is a function that creates and initializes an instance of a struct. The term "constructor" is borrowed from object-oriented programming (OOP), but Go doesn't have constructors as part of the language syntax (like in Java or C++).
Instead, constructors in Go are just conventional functions that return a pointer to a struct. It typically named NewTypeName
(e.g., NewUser
). Focuses on initializing a single type with its required or default values.
package main
import "fmt"
type User struct {
Name string
Email string
}
func NewUser(name, email string) *User {
return &User{Name: name, Email: email}
}
func main() {
user := NewUser("John Doe", "john@example.com")
fmt.Println(user)
}
Run (opens in a new tab) this code in your browser.
Use constructors when you want a simple, standard way to initialize a specific type with required parameters.
Creator Pattern
A creator pattern in Go is a more generalized and flexible approach to object creation. It often abstracts the creation logic and can be used to create objects of various types or with more complex initialization requirements. This is the main difference between a constructor and a creator function.
package main
import "fmt"
// Product interface for different types of products
type Product interface {
GetDetails() string
}
// Concrete Product A
type ProductA struct {
Name string
}
func (p ProductA) GetDetails() string {
return "Product A: " + p.Name
}
// Concrete Product B
type ProductB struct {
Name string
}
func (p ProductB) GetDetails() string {
return "Product B: " + p.Name
}
// Creator function
func CreateProduct(productType string, name string) Product {
switch productType {
case "A":
return ProductA{Name: name}
case "B":
return ProductB{Name: name}
default:
return nil
}
}
func main() {
product := CreateProduct("A", "Gadget")
fmt.Println(product.GetDetails())
}
Run the code above here (opens in a new tab)
Use creator functions when you need more flexible and abstract ways to create objects, especially when creating multiple types or configurations.
Builder Pattern
The Builder pattern is used to construct complex objects step by step. It is particularly useful when a complex object has many optional parameters.
package main
import "fmt"
// Car struct with multiple fields
type Car struct {
Make string
Model string
Color string
Year int
}
// CarBuilder helps construct a Car object
type CarBuilder struct {
car *Car
}
func NewCarBuilder() *CarBuilder {
return &CarBuilder{car: &Car{}}
}
func (cb *CarBuilder) SetMake(make string) *CarBuilder {
cb.car.Make = make
return cb
}
func (cb *CarBuilder) SetModel(model string) *CarBuilder {
cb.car.Model = model
return cb
}
func (cb *CarBuilder) SetColor(color string) *CarBuilder {
cb.car.Color = color
return cb
}
func (cb *CarBuilder) SetYear(year int) *CarBuilder {
cb.car.Year = year
return cb
}
func (cb *CarBuilder) Build() *Car {
return cb.car
}
func main() {
car := NewCarBuilder().
SetMake("Toyota").
SetModel("Corolla").
SetColor("Blue").
SetYear(2021).
Build()
fmt.Println(car)
}
Try (opens in a new tab) out the pattern without installation.
It simplifies the construction of complex objects avoiding large constructors with many arguments.
Option Pattern
The Option pattern provides flexibility in object initialization by allowing optional parameters through functional options. This pattern is ideal for cases where a struct has many fields, but most are optional. It brings an other solution to the same problem like the builder pattern, but it targets less complex objects.
package main
type Server struct {
Host string
Port int
Protocol string
Timeout int
}
type OptionFunc func(*Server)
func (o OptionFunc) apply(server *Server) {
o(server)
}
type Option interface {
apply(*Server)
}
func NewServer(host string, port int, options ...Option) *Server {
server := &Server{
Host: host,
Port: port,
}
for _, opt := range options {
opt.apply(server)
}
return server
}
func WithProtocol(protocol string) Option {
return OptionFunc(func(s *Server) {
s.Protocol = protocol
})
}
func WithTimeout(timeout int) Option {
return OptionFunc(func(s *Server) {
s.Timeout = timeout
})
}
It reduces boilerplate code and allows flexibility in configuration without overloading constructors.
Enum Pattern
While Go does not have built-in support for enums, you can emulate enums using custom types and constants.
package main
import "fmt"
// LogLevel represents a logging level
type LogLevel int
const (
Debug LogLevel = iota
Info
Warn
Error
)
func (l LogLevel) String() string {
return [...]string{"Debug", "Info", "Warn", "Error"}[l]
}
func Log(level LogLevel, message string) {
fmt.Printf("[%s] %s\n", level, message)
}
func main() {
Log(Info, "Application started")
Log(Error, "An error occurred")
}
Run the code above in your browser (opens in a new tab)
It provides type safety and readable constants. It is easy to expand and maintain
Singleton
Ensures only one instance of a type exists globally
package main
import "sync"
type Singleton struct{}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
Run the code above in your browser (opens in a new tab)
Strategy
Encapsulates interchangeable algorithms
type Strategy interface {
Execute() string
}
type ConcreteStrategyA struct{}
func (ConcreteStrategyA) Execute() string { return "Strategy A" }
type ConcreteStrategyB struct{}
func (ConcreteStrategyB) Execute() string { return "Strategy B" }
func main() {
var strategy Strategy = ConcreteStrategyA{}
fmt.Println(strategy.Execute())
strategy = ConcreteStrategyB{}
fmt.Println(strategy.Execute())
}
Run the code above in your browser (opens in a new tab)