Go Constructors Tutorial
Welcome Gophers! 👋
In this tutorial, we are going to be covering the concept of constructors in Go.
Go Isn’t Object Oriented
It’s worth pointing out that, Go itself is not an object-oriented language the same way that the likes of Java is. Constructors aren’t something that are built in to the language and what I demonstrate below show the equivalents to constructors in Go, but they are more akin to factory functions.
It’s fairly common practice to use this approach when creating new structs
that require a more-involved setup than just zero-valuing fields on the struct.
What Are Constructors?
Let’s start off with the theory. What are constructors, and what do they do for us?
Well, when you are building Go applications, you will tend to want to build modular components that are loosely coupled to one another. This is becoming a more popular approach as more Go developers are starting to adopt hexagonal architectures when designing their apps.
Most of these components could look a little something like below. We would have a Component
struct and off that struct we would have a number of methods.
These methods typically end up using some of the fields defined within the Component
struct like a service that interacts with a database/queue etc.
package user
// Component
type Service struct {
// it will likely have some dependencies on other components
CommentRepo CommentRepo
}
// these dependencies should be modelled as interfaces to help ensure
// our components are loosely coupled.
type CommentRepo interface {
GetComments() ([]Comment, error)
}
// Comment - in this example, imagine our component is doing
// stuff processing of comments on this website.
type Comment struct {
Author string
Body string
Slug string
}
// NewService - our constructor function
func NewService(cmtRepo CommentRepo) (*Service, error) {
svc := &Service{
CommentRepo: cmtRepo,
}
// handles other potentially more complex setup logic
// for our component, there could be calls to downstream
// dependencies to check connections etc that could return
// errors
return svc, nil
}
// DoesStuff - a method that takes a pointer receiver to an
// instantiated Component
func (c *Component) DoesStuff() error {
comments, err := c.Service.GetComments()
if err != nil {
return err
}
// do additional things with the returned comments
}
The constructor, in this case, is our NewService
function that returns a pointer to an instantiated
component or an error if there are any errors when setting up this component.
// NewService - our constructor function
func NewService(cmtRepo CommentRepo) (*Service, error) {
svc := &Service{
CommentRepo: cmtRepo,
}
// handles other potentially more complex setup logic
// for our component, there could be calls to downstream
// dependencies to check connections etc that could return
// errors
return svc, nil
}
Now, if we want to instantiate this component, the code would end up looking something like this:
package main
func Run() error {
// instantiate any dependencies my Component struct will need
// in this example, let's imagine the comment package an implementation
// that matches what our Service expects
commentRepo := comment.NewRepo(dbConnectionInfo)
// We can then pass this into our NewService constructor like so:
comp, err := user.NewService(commentRepo)
if err != nil {
// handle this properly with logs/metrics/alerts etc
return err
}
return nil
}
func main() {
if err := Run(); err != nil {
log.Fatal(err.Error())
}
}
Note - this is an example of ‘accepting interfaces, returning structs’ in Go.
When I’m designing any Go applications I tend to follow the same pattern as above. The reason is that it allows me to simplify my applications.
If the above example had a dependency on a Queue
component for example, this Component
wouldn’t necessarily care about the implementation details of whatever Queue
implementation is passed in, it would just expect it to implement the interface defined within this component itself.
The Built-in new Function
Now, the alternative approach is to use the built-in new
function within Go that will return a pointer to whatever type you want and instantiate everything within that type to zero-values.
This is useful in the case where you are instantiating something that doesn’t have any complex setup logic or require initialization to be done within the struct itself.
Let’s take a look with a small example:
package main
import "fmt"
type Comment struct {
Author string
Body string
Slug string
ID int
}
func main() {
cmt := new(Comment)
fmt.Printf("%+v\n", cmt)
}
Now, when we run this code, we should see that the first Comment
we create using the new
built-in function returns a pointer to a new Comment. This is denoted by the &
at the start of the printed-out comment.
$ go run main.go
&{Author: Body: Slug: ID:0}
Conclusion
Awesome, so in this tutorial, we’ve covered the concept of writing constructor functions within your Go applications that follow the practice of accepting interfaces, returning structs.
We’ve also looked at how you can instantiate pointers to less-complex structs that require no additional instantiation using the built-in new()
function which just does the job of allocating zeroed storage for a new Comment
type and returns the pointer address of this newly allocated Comment
.