Builder Design Pattern in Go
Builder Pattern in Go
Quick backstory
The Problem
The problem I am facing is that I have a particular workflow for writing Go code, which gets repetitive. Even when working on starting a new project, a general idea, or testing a quick theory, the setup is a bit cumbersome for me. Here is an example of the project structure I typically start with:
└── folder1
├── main.go
├── go.mod
├── README.md
└── Makefile
It felt like a lot to run go mod init github.com/EzlosSWM/ and touch main.go README.md Makefile every time - let alone boilerplate content for each file. So that is when I decided, in typical programmer fashion, to write a script for generating the project structure.
Note: the script and the breakdown will come after.
I managed to write a simple enough script in Go to get it out of the way, but I knew; I could do better. That’s when I started brainstorming ways to make it scaleable & easier to maintain. I wanted a CLI tool that generates boilerplate Go code for a generic project and eventually scales up to complete projects with different architectural patterns.
What is the builder pattern?
Essentially, a builder is a creational design pattern that allows you to create objects (in Go’s case, structs). Encapsulating the logic for creation allows for cleaner and easier-to-maintain code while being flexible. It defines a predetermined sequence of steps to promote consistency and reliability.
Imagine having to build n users with different clearance levels. It would be much simpler to have a checklist of data a user needs to have. For example:
- setAge()
- setPhone()
- setAdminStatus()
Go example (easy)
- Initializing the project
// main.go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, world!")
}
Now just run go run main.go
to get:
$ Hello, world!
- Defining our interface
Under our import section, we want to define our builder interface to house our API.
type Builder interface {
setAge(int) User
setPhone(int) User
setAdminStatus(bool) User
}
- Define our User struct
type User struct {
name string
age int
phone int
isAdmin bool
}
- Define our functions to instantiate our User struct & function
// instantiates our User struct
func newUser(name string) *User {
return &User{
name: name,
}
}
// attaches our methods to the User struct
func (u *User) setAge(age int) *User {
u.age = age
return u
}
func (u *User) setPhone(phone int) *User {
u.phone = phone
return u
}
func (u *User) setAdminStatus(status bool) *User {
u.isAdmin = status
return u
}
- Building our users
Now that we’ve done most of the heavy lifting, we can start by using our builder to create users quickly and effectively.
func main() {
regularUser := newUser("foo").setAge(18).setPhone(123456789)
fmt.Println(regularUser)
adminUser := newUser("bar").setAge(20).setAdminStatus(true)
fmt.Println(adminUser)
}
Output:
&{foo 18 123456789 false}
&{bar 20 0 true}
Hurray! We just made a User builder.
Full code example
package main
import "fmt"
type Builder interface {
setAge(int) User
setPhone(int) User
setAdminStatus(bool) User
}
type User struct {
name string
age int
phone int
isAdmin bool
}
func newUser(name string) *User {
return &User{
name: name,
}
}
func (u *User) setAge(age int) *User {
u.age = age
return u
}
func (u *User) setPhone(phone int) *User {
u.phone = phone
return u
}
func (u *User) setAdminStatus(status bool) *User {
u.isAdmin = status
return u
}
func main() {
regularUser := newUser("foo").setAge(18).setPhone(123456789)
fmt.Println(regularUser)
adminUser := newUser("bar").setAge(20).setAdminStatus(true)
fmt.Println(adminUser)
}
// Output
&{foo 18 123456789 false}
&{bar 20 0 true}
Conclusion
The builder pattern is an easy-to-implement creational design pattern, and if implemented correctly - scales easily. In this scenario, we have a builder that creates regular and admin users, but there are multiple scenarios where this would be useful.