Blog Cover

Software design patterns: the Builder pattern in Go

Author profile image
Aitor Alonso

Dec 23, 2023

4 min read

The builder pattern is a technique where a developer uses a "builder" to construct an object. The end result could be anything - a string, a struct instance, or even a closure - but it is built using a builder. More often than not a builder is used because the object being created is complex and needs to be constructed in multiple steps, so the builder helps isolate each step and prevent bugs.

Imagine a complex object that requires laborious, step-by-step initialization of many fields and nested objects. Such initialization code is usually buried inside a monstrous constructor with lots of parameters. Or even worse: scattered all over the client code. Here is where the builder pattern comes to the rescue.

To illustrate the idea, I'll provide an easy example, for what the Builder pattern would be an overkill, but it'll help us understand this. Obviously, as the title of this article says, I'll be using Go, but note that this is an universal software design pattern, so it can be applied to any language that supports some kind of Object Oriented Programming.

Naive example

Imagine you have an Car struct:

type Car struct {
  Model string
  Type string
  Traction string
}

Under most circumstances a developer would create an instance of the Car struct in their code and assign a value to each of these fields, but in some circumstances you might want to do more than that. For instance, you might decide that when the Type field is set you can also assign the Traction field based on the car's type. This could be achieved using a builder.

type Builder struct {
  c Car
}

func (b *Builder) Build() Car {
  return b.c
}

func (b *Builder) Model(model string) *Builder {
  b.c.Model = model
  return b
}

func (b *Builder) Type(carType string) *Builder {
  if carType == "SUV" {
    b.c.Traction = "4x4"
  } else {
    b.c.Traction = "front"
  }
  b.c.Type = carType
  return b
}

Now when we are constructing a Car we can use the builder to ensure that the Traction gets set when we set the Type field. We could even add validation in and verify that the Type is a valid one - though at that point we would need to also return an error.

Below we can see how this builder might be used (Go Playground link):

b := &Builder{}
car := b.
  Model("Toyota Rav4").
  Type("SUV").
  Build()
fmt.Println(car)
// {Toyota Rav4 SUV 4x4}

The magic of the builder pattern is that we can chain the builder methods together, and each method returns the builder itself. This allows us to call the next method on the builder, and so on. The last method called is the Build method, which returns the final object.

As said, this is a really basic example so the builder pattern might feel like overkill, but much more complex scenarios exist where the builder pattern is incredibly helpful. For instance, constructing SQL queries can become complex and we might need to handle conditional queries. In this case a builder can simplify things a bit.

Real-world™ examples

Indeed, most SQL query builder libs out there implement this pattern. One of such libraries is squirrel. Let's take a look at a few examples from the squirrel docs at the moment of writing this article. First, we start with a basic example of how squirrel can be used to generate the SQL query string we might use with the standard library's database/sql package.

users := sq.Select("*").From("users").Join("emails USING (email_id)")
sql, args, err := users.ToSql()
// sql == "SELECT * FROM users JOIN emails USING (email_id)"

active := users.Where(sq.Eq{"deleted_at": nil})
sql, args, err := active.ToSql()
// sql == "SELECT * FROM users JOIN emails USING (email_id) WHERE deleted_at IS NULL"

Now, let's move to a more complex scenario. Let's say we want to filter by name is a certain variable q has content, which is as simple as adding an if statement and adding the conditional query

if len(q) > 0 {
  users = users.Where("name LIKE ?", fmt.Sprint("%", q, "%"))
}

Notice how we assign the result of the users.Where function call to the users variable, allowing us to retain this new build step. Now whenever we call users.ToSql() (that acts like the Build() function in the Car example) it will have the Where clause here only if the if statement was true.

Builders can even be found in Go's standard library too. See text/template and html/template packages. Both of the template packages have a Template type that uses the builder pattern:

tmpl, err := template.New("titleTest").
  Funcs(funcMap).
  Delims("[[", "]]").
  Parse(templateText)

Both Funcs and Delims return a *Template, and Parse returns both a *Template and an error kinda like our Role example in the employee builder would have if we validated the role.

Conclusion

To sum up - the builder pattern usually involves chaining functions on a single type as we "build" the end result. It is used to build complex objects, and can be found in quite a few libraries.

If you want to go deeper into the Builder pattern and others, I invite you to take a look at the refactoring.guru page. It's a great resource to learn about software design patterns. Also, if you want to read more from me, you can check other patterns articles in my blog.


I hope my article has helped you, or at least, that you have enjoyed reading it. I do this for fun and I don't need money to keep the blog running. However, if you'd like to show your gratitude, you can pay for my next coffee(s) with a one-time donation of just $1.00. Thank you!