The idea is simple: Go structs don’t have a built-in notion of “required fields”. Whenever a new field that is required from a business/product perspective is added, you have to manually update every place where the struct is constructed. This is easy to miss and can lead to bugs.
Constructors can help, but they don’t scale well as the number of fields grows.
This tool generates builders where required fields must be provided before Build() is available, so missing required fields become compile-time errors.
Example:
user := NewUserBuilder().
WithName("John").
WithAge(22).
Build()
If Age is required and you skip it, the code won’t compile.
Moin,
the way I do it is to use a constructor.
This will print errors as shown below, so you only have to change the struct and the constructor once you make a change.
For the null value “issue” that comes along with it - causes it, as you can not define behaviours of stuff that does not exist in that language.
I use pointers, or nested key with Boolean Valid as the same pattern is used in databases ↔ Go as shown here.
And then there are lots of cases where the default value is always ok.
Remember: This is not overhead in Go compared to Python. It might look like overhead, but in truth other languages have this stuff not visible to the user for everything (even if it might not be needed for the app - leading to slow code)
So here is a possible example:
package main
import "fmt"
type NameInfo struct {
Value string
Valid bool
}
type Vertex struct {
X, Y int
Name NameInfo
}
func main() {
a := NewVertex(1, 0) // not enough arguments in call to NewVertex
// have (number, number)
// want (int, int, string)compilerWrongArgCount
fmt.Println(a)
}
func NewVertex(x int, y int, name string) Vertex {
return Vertex{
X: x,
Y: y,
Name: NameInfo{Value: name, Valid: true},
}
}```
papa-carlo allows placing builders in packages different from the struct’s package. When it happens, the builder cannot work with fields that are private or that use unexported types. Therefore the tool will intentionally fail if such fields are not omitted.
… is a problem. Because nothing would stop me from from doing this:
user := NewUserBuilder().
WithName("John").
WithAge(22).
Build()
// Don't tell me what to do!
user.Name = ""
Also - if you’re going to require a Build() function, you could just shift your validation into that function:
Also - almost universally, the place where this matters is when you go to actually do something with a struct:
// Doesn't matter that Name/Age is zero value
user := User{}
// Here is where it DOES matter that Name/Age is zero value. Makes sense
// to shift the onus for validation to where the validation matters. So
// this func could just validate and return an error.
func DoSomething(u User) error
Anyway, I think the idea of compile-time enforcement of required fields is interesting. But to Karl’s point, I think constructors are how most people solve this problem. And you can’t guarantee that something didn’t mutate your object after the required fields were initialized anyway, so you have to validate state where you’re using it.