title

Struct initialization in Go is simple, but enforcing mandatory fields can quickly become tricky in larger codebases. Let’s walk through the problem and explore a possible solution.

Named field initialization

One common way to create struct instances is by specifying fields by name.

type Vertex struct {
	X int
	Y int
}

fmt.Println(Vertex{X: 1, Y: 2})
// {1 2}

This allows us to omit fields at creation time, which will make Go fall back to the default value for the omitted fields:

type Vertex struct {
	X int
	Y int
}

fmt.Println(Vertex{X: 1})
// {1 0}

Mandatory fields problem

In some languages, developers can specify what fields are mandatory to be set at the moment of the object creation. Here’s a Python example:

@dataclass
class User:
    name: str
    age: int

User(name="Alice")  # TypeError - age is missing

But in Go, there’s no such mechanism. This might become a problem: imagine you add a new mandatory field to the struct, you don’t want it to fall back to the default value. You need to update all places where the struct is initialized and this cannot be enforced by the compiler. If the developer wants to ensure that all mandatory fields are set, they should create struct objects via positional initialization or agree on using constructors:

// positional initialization
v := Vertex{1} // won't compile
v := Vertex{1, 2} // OK

// constructor pattern
func New(x int, y int) Vertex {
    return Vertex{X: x, Y: y}
}

v := New(1, 2)

Positional initialization

Some people may not prefer using the positional initialization because it relies on the order of fields in the struct. If someone changes the order of the fields in the struct definition, it will silently change the behavior of the object initializations:

// refactored the order of fields for readability purposes:
type Vertex struct {
    Y int // was second now first
    X int // was first now second
}

v := Vertex{1, 2} // all places with positional initialization need to be updated if type checks still pass

Constructors

Now let’s assume it was agreed to use constructors for creating struct objects. This pattern may become hard to use if the number of fields grows. It harms readability, or it makes developers leave comments on every argument - which is subject to errors:

func NewUser(firstName string, lastName string, age int, height int, weight int) User {
    ...
}


u := NewUser("John", "Doe", 45, 180, 80) // not very readable when the list of arguments is long

// can be annoying writing this way + the comments might be wrong
u := NewUser(
    "John", // firstName
    "Doe", // lastName
    45, // age
    180, // height
    80, // weight
)

Builder pattern

Another approach could be to use factory or builder patterns.

u := NewUserBuilder().
    WithFirstName("John").
    WithLastName("Doe").
    WithAge(45).
    WithHeight(180).
    WithWeight(80).
    Build()

At the moment of writing this post, I could not find a library that would let me mark fields as mandatory to make the compiler fail if a mandatory field is missing. A typical builder library would generate builder code like this:

type UserBuilder struct {
    u User
}

NewUserBuilder() UserBuilder { ... }

func (ub *UserBuilder) WithFirstName(firstName string) { ub.u.FirstName = firstName }

func (ub *UserBuilder) WithLastName(lastName string) { ub.u.LastName = lastName }

...

func (ub *UserBuilder) Build() User { return ub.u }

// this is still possible and violates the mandatory field requirement
u := NewUserBuilder().WithFirstName("John").Build()

papa-carlo approach

That motivated me to write a tool called papa-carlo which can generate builders that satisfy the mandatory field requirement. An idea was suggested by a colleague of mine: it works by chaining field builders where each builder has only one method associated with one mandatory field. Only the final builder associated with the last mandatory field leads to the builder finalization with the Build() method that produces the target object. A hypothetical example:

// first field
type BuilderForFirstName struct {
    u User
}

type BuilderForLastName struct { ... }

...

// last field
type BuilderForWeight struct { ... }

type FinalizationBuilder struct { ... }

// -------------------------------------------------------------------------------

func NewUserBuilder() BuilderForFirstName { return BuilderForFirstName{} }

func (b *BuilderForFirstName) WithFirstName(firstName string) BuilderForLastName{
    u.FirstName = firstName
    return BuilderForLastName{u: b.u}
}

...

func (b *BuilderForWeight) WithWeight(weight int) FinalizationBuilder { ... }

func (b *FinalizationBuilder) Build() User { ... }

If a new mandatory field FootSize is added, the builder won’t allow you to create a struct instance unless it was set because WithWeight would return the new BuilderForFootSize and not FinalizationBuilder.

These builders are also nicely picked up by IntelliSense: usage example

One could argue that with AI tools, we don’t need to think this deeply about struct initialization, and we can rely on AI to do struct initialization right. I can see how some developers would still prefer creating compiler guardrails so that they can close the loop with AI more effectively.

Others might say that creating big structs is not idiomatic in Go. That might be true but I saw companies using Go for business layers where having large structs was hard to avoid. Even if there’s a way to rewrite it in a more Go style, I believe there are people that need to embrace the complexity of their business domain and the existing code, therefore they might find a good use of this builder.

In any case, this tool was mostly written for fun — so enjoy!