Building Complex Objects in Go: A Guide to the Builder Pattern

dsysd dev
5 min readMay 12

--

Photo by Michal Matlon on Unsplash

Creating complex objects with many optional parameters can be a daunting task.

The traditional constructor and setter approach can become cumbersome when dealing with objects with many optional parameters.

In this article, we will explore the builder pattern, a creational design pattern that allows for the creation of complex objects with many optional parameters.

We will walk through an example implementation of the builder pattern in Go and discuss how it can be used to create different variations of the same object.

+----------------+               +-------------------+
| Director | directs | Builder |
+----------------+ +-------------------+
| construct() | ------------> | buildPart1() |
| setBuilder() | | buildPart2() |
+----------------+ | getProduct() |
+-------------------+
|
V
+-------------------+
| Product |
+-------------------+
| field1 |
| field2 |
| ... |
+-------------------+

Implementing the builder pattern in go

Here is an example of the builder pattern in golang

type Car struct {
Make string
Model string
Year int
Color string
EngineSize float64
}

type CarBuilder struct {
Car
}

func (cb *CarBuilder) SetMake(make string) *CarBuilder {
cb.Make = make
return cb
}

func (cb *CarBuilder) SetModel(model string) *CarBuilder {
cb.Model = model
return cb
}

func (cb *CarBuilder) SetYear(year int) *CarBuilder {
cb.Year = year
return cb
}

func (cb *CarBuilder) SetColor(color string) *CarBuilder {
cb.Color = color
return cb
}

func (cb *CarBuilder) SetEngineSize(engineSize float64) *CarBuilder {
cb.EngineSize = engineSize
return cb
}

func (cb *CarBuilder) Build() *Car {
return &cb.Car
}

The CarBuilder struct embeds a Car object, so all of its fields are available to the builder.

The CarBuilder struct has methods to set the optional parameters of the Car object. Each method returns a pointer to the CarBuilder struct to allow for method chaining.

The Build method on the CarBuilder struct returns a pointer to the Car object that was built.

Here’s an example usage of the CarBuilder:

carBuilder := &CarBuilder{}

car := carBuilder.
SetMake("Toyota").
SetModel("Corolla").
SetYear(2021).
SetColor("Red").
SetEngineSize(1.8).
Build()

fmt.Printf("Make: %s\n", car.Make) // Output: Make: Toyota
fmt.Printf("Model: %s\n", car.Model) // Output: Model: Corolla
fmt.Printf("Year: %d\n", car.Year) // Output: Year: 2021
fmt.Printf("Color: %s\n", car.Color) // Output: Color: Red
fmt.Printf("Engine Size: %.1f\n", car.EngineSize) // Output: Engine Size: 1.8

In this example, we create a CarBuilder object and use its methods to set the optional parameters of the Car object.

Finally, we call the Build method to get the final Car object. We then print out the fields of the Car object to verify that they were set correctly.

Advanced Use Cases of the Builder Pattern in Go

The builder pattern has some advanced use cases that can be useful in certain situations.

We will now cover some advanced use cases of the builder pattern in Go.

Creating a Builder Interface

In the basic example of the builder pattern, we had a single builder struct that was used to build an object.

However, you can create a builder interface that multiple builder structs can implement.

This can be useful when you have different types of objects that need to be built using the same pattern.

Let’s say that you have two types of cars: electric cars and gasoline cars. Both types of cars have the same optional parameters, but they have different required parameters.

In this case, you can create a CarBuilder interface that specifies the required methods for building a car, and then create two structs that implement the CarBuilder interface: ElectricCarBuilder and GasolineCarBuilder.

type CarBuilder interface {
SetMake(make string) CarBuilder
SetModel(model string) CarBuilder
SetYear(year int) CarBuilder
SetColor(color string) CarBuilder
SetEngineSize(engineSize float64) CarBuilder
Build() Car
}

type ElectricCarBuilder struct {
Car
}

type GasolineCarBuilder struct {
Car
}

Both ElectricCarBuilder and GasolineCarBuilder embed the Car struct and implement the CarBuilder interface.

They can then have their own implementation of the required methods for building a car.

func (b *ElectricCarBuilder) SetMake(make string) CarBuilder {
b.Make = make
return b
}

func (b *ElectricCarBuilder) SetModel(model string) CarBuilder {
b.Model = model
return b
}

func (b *ElectricCarBuilder) SetYear(year int) CarBuilder {
b.Year = year
return b
}

func (b *ElectricCarBuilder) SetColor(color string) CarBuilder {
b.Color = color
return b
}

func (b *ElectricCarBuilder) SetEngineSize(engineSize float64) CarBuilder {
b.EngineSize = engineSize
return b
}

func (b *ElectricCarBuilder) Build() Car {
return b.Car
}

func (b *GasolineCarBuilder) SetMake(make string) CarBuilder {
b.Make = make
return b
}

func (b *GasolineCarBuilder) SetModel(model string) CarBuilder {
b.Model = model
return b
}

func (b *GasolineCarBuilder) SetYear(year int) CarBuilder {
b.Year = year
return b
}

func (b *GasolineCarBuilder) SetColor(color string) CarBuilder {
b.Color = color
return b
}

func (b *GasolineCarBuilder) SetEngineSize(engineSize float64) CarBuilder {
b.EngineSize = engineSize
return b
}

func (b *GasolineCarBuilder) Build() Car {
return b.Car
}

This is how we can create car using the interface, we can also use the same interface for mocking.

func CreateCar(builder CarBuilder) Car {
return builder.
SetMake("Toyota").
SetModel("Corolla").
SetYear(2022).
SetColor("blue").
SetEngineSize(2.0).
Build()
}

func main() {
electricCarBuilder := &ElectricCarBuilder{}
gasolineCarBuilder := &GasolineCarBuilder{}

electricCar := CreateCar(electricCarBuilder)
gasolineCar := CreateCar(gasolineCarBuilder)

fmt.Printf("Electric car: %+v\n", electricCar)
fmt.Printf("Gasoline car: %+v\n", gasolineCar)
}

In this example, we create an ElectricCarBuilder and a GasolineCarBuilder, and use them to create an electric car and a gasoline car, respectively.

The CreateCar function takes a CarBuilder interface and sets the required fields using the builder's methods, before finally calling the Build method to create the Car object.

Subscribe to my Youtube channel

Subscribe to my youtube channel if you are on the lookout for more such awesome content in video format.

https://www.youtube.com/@dsysd-dev

Claps Please !!

If you found this article helpful I would appreciate some claps 👏👏👏👏, it motivates me to write more such useful articles in the future.

Follow me on medium for regular awesome content and insights.

Subscribe to my Newsletter

If you like my content, then consider subscribing to my free newsletter, to get exclusive, educational, technical, interesting and career related content directly delivered to your inbox

https://dsysd.beehiiv.com/subscribe

Important Links

Thanks for reading the post, be sure to follow the links below for even more awesome content in the future.

Twitter: https://twitter.com/dsysd_dev
Youtube: https://www.youtube.com/@dsysd-dev
Github: https://github.com/dsysd-dev
Medium: https://medium.com/@dsysd-dev
Email: dsysd.mail@gmail.com
Linkedin: https://www.linkedin.com/in/dsysd-dev/
Newsletter: https://dsysd.beehiiv.com/subscribe
Gumroad: https://dsysd.gumroad.com/

--

--

dsysd dev

Helping you become an 11x developer. I write on distributed systems, system design, blockchain, and go. https://twitter.com/dsysd_dev