Go Constructors Tutorial

Elliot Forbes Elliot Forbes ⏰ 5 Minutes 📅 Feb 18, 2022

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.

user.go
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:

main.go
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.

Additional Reading