A constructor is a language feature that is used to initialize variables. The Go language doesn’t have any concrete “constructor” as such, but we can use a number of different language features to idiomatically initialize variables.

In this post, we will go through the different types of constructors that we can use in Go, and in what situations you should use them.

banner

You can view the code to run all examples mentioned here on the Go playground

Using Composite Literals

Composite literals are the most straight-forward way to initialize an object in Go.

Constructing a variable using composite literals looks like this:

// creating a new struct instance
b := Book{}

// creating a pointer to a struct instance
bp := &Book{}

// creating an empty value
nothing := struct{}{}

For the above examples, let’s assume we have a Book struct defined:

type Book struct {
  title string
  pages int
}

Now, we can initialize a new Book instance with its attributes set:

b := Book{
  title: "Julius Caesar",
  pages: 322,
}

The advantage of using composite literals is that they have a straightforward syntax that’s easy to read.

However, we cannot set default values to each attribute. So, when we have structs that contain many default fields, we would need to repeat the default value for each instantiation.

For example, consider a Pizza struct, where we want six slices by default:

type Pizza struct {
  slices int
  toppings []string
}

somePizza := Pizza{
  slices: 6,
  toppings: []string{"pepperoni"},
}

otherPizza := Pizza{
  slices: 6,
  toppings: []string{"onion", "pineapple"},
}

In this example, we have to keep setting the number of slices as 6 each time.

Additionally, when we have nullable attributes like toppings, it would be set to nil if not explicitly initialized. This can be error prone if your code assumes that each attribute is initialized.

Custom Constructor Functions

Making your own constructor function can be useful if you need to set defaults or perform some initialization steps beforehand.

Let’s look at how we can construct our Pizza instance better by creating a NewPizza function:

func NewPizza(toppings []string) () {
  if toppings == nil {
    toppings = []string{}
  }
  return Pizza{
    slices: 6,
    toppings: toppings,
  }
}

By using a constructor function, we are able to customize how our instance is created:

  1. We can have default values for fields that we don’t want to set initially. We can make use of optional parameters if we want to choose which fields we want to set and which ones we want to default.
  2. We can perform sanity checks - like checking whether toppings is nil and defaulting to an empty slice of strings.

There are also some special functions in Go, like make or new, which can construct certain data types with more control over memory and capacity.

Returning Errors from Constructors

While constructing your variable, we are sometimes dependent on other systems or libraries that may fail.

In these cases, it’s better to return an error along with the initialized value.

func NewRemotePizza(url string) (Pizza, error) {
  // toppings are received from a remote URL, which may fail
  toppings, err := getToppings(url)
  if err != nil {
    // if an error occurs, return the wrapped error along with an empty
    // Pizza instance
    return Pizza{}, fmt.Errorf("could not construct new Pizza: %v", err)
  }
  return Pizza{
    slices:   6,
    toppings: toppings,
  }, nil
}

A returned error helps in encapsulating failure conditions within the constructor itself.

Interface Constructors

A constructor can return an interface directly, while initializing a concrete type within.

This can be helpful if we want to make the struct private while making its initialization public.

Let’s see how we can apply this to our Pizza: if we have a bakery, we can consider a pizza as just one of many “bakeable” items.

We can create a Bakeable interface to represent this, and add a new isBaked field to the Pizza struct:

type Pizza struct {
	slices   int
	toppings []string
	isBaked  bool
}

func (p Pizza) Bake() {
	p.isBaked = true
}

// Pizza implements Bakeable
type Bakeable interface {
	Bake()
}

// this constructor will return a `Bakeable`
// and not a `Pizza`
func NewUnbakedPizza(toppings []string) Bakeable {
	return Pizza{
		slices:   6,
		toppings: toppings,
	}
}

Best Practices for Idiomatic Constructors

Let’s look at some conventions around naming and organizing constructor functions in Go.

Basic Constructors

For simple constructor functions returning a type (example: Abc, or Xyz), the functions should be named NewAbc or NewXyz respectively.

We saw this in practice when we defined the NewPizza function to initialize a Pizza instance.

Primary Package Types

If the variable being initialized is the primary type for the given package, we can name our function New (without any suffix).

For example, if our Pizza struct was defined in a pizza package, our constructor would look like this:

package pizza

type Pizza struct {
  // ...
}

func New(toppings []string) Pizza {
  // ...
}

Now, the code for calling this function from another package would read as p := pizza.New()

Variations

In many cases, there would be multiple ways to construct the same type.

To accommodate this, we can use variations of the NewXyz name to describe each method.

For example, we saw that there were multiple ways to create a Pizza:

  1. NewPizza was the primary constructor.
  2. NewRemotePizza constructs a pizza from a remote resource.
  3. NewUnbakedPizza returns a pizza as a Bakeable interface.

You can view the code to run all examples mentioned here on the Go playground