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.
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:
- 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.
- We can perform sanity checks - like checking whether
toppings
isnil
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
:
NewPizza
was the primary constructor.NewRemotePizza
constructs a pizza from a remote resource.NewUnbakedPizza
returns a pizza as aBakeable
interface.
You can view the code to run all examples mentioned here on the Go playground