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.