Blog Post
Go: Understanding Constructor Functions
Exploring the idiomatic approach to object initialization in Go and understanding why we use constructor functions to create new struct instances.
As I continue my journey into Go, I’ve been exploring how the language handles object-oriented concepts. Coming from languages with traditional classes, one of the first things I noticed is that Go doesn’t have a built-in constructor keyword or a new Class() syntax.
Instead, Go relies on a simple, idiomatic pattern: Constructor Functions.
What is a Constructor Function?
A constructor function is just a regular Go function that creates and returns a new instance of a struct. By convention, if your struct is named User, the constructor function is typically named NewUser.
Here is an example I’ve been working with, along with a comment summarizing my understanding:
// A constructor function is a function that creates and returns a new instance of a struct.// It is a common pattern in Go to use constructor functions to create new instances of structs,// especially when the struct has many fields or when we want to enforce certain invariants.// We use pointers here because we want to return a reference to the struct, rather than a copy of it.// This allows us to modify the struct through the pointer and have those changes reflected in the original struct.func newUser(name, email string, age int) *User { return &User{ Name: name, Email: email, Age: age, }}(Note: If this function needs to be exported and usable by other packages, it should be capitalized as NewUser!)
Why Do We Use Them?
While you can always initialize a struct directly (e.g., user := User{Name: "Bob"}), constructor functions provide several critical benefits:
1. Enforcing Invariants
If a User must have a valid email and must be over 18, a constructor function gives us a place to validate this before the object is ever created. We can return an error if the invariants aren’t met:
func NewUser(name, email string, age int) (*User, error) { if age < 18 { return nil, errors.New("user must be at least 18") }
return &User{ Name: name, Email: email, Age: age, }, nil}2. Complex Initialization
Sometimes a struct needs more than just simple field assignment. It might need a database connection, an initialized map (e.g., make(map[string]int)), or background goroutines started. The constructor encapsulates all of this setup so the caller doesn’t have to worry about it.
3. Returning Pointers for State Mutation
As noted in my comment above, returning a pointer (*User) instead of a value (User) has two main benefits:
- Efficiency: We avoid copying the entire struct when returning it.
- Mutability: Any methods called on the returned pointer will modify the original struct. If the struct represents stateful data (like a repository or a database connection), we almost always want to pass it around by reference.
Conclusion
Go’s approach to constructors felt very manual at first, but I’ve come to appreciate its transparency. There is no hidden “magic” happening under the hood when you initialize an object—it’s just a function returning a struct pointer. It keeps the code readable, explicit, and distinctly Go.