Background on Go and Struct Initialization

Go has a special character. It differs from other attractive languages out there. Go constructors are unique too. Let's establish a baseline by comparing how other languages construct objects.

PHP

<?php
class Point {
    protected int $x;
    protected int $y;

    public function __construct(int $x, int $y = 0) {
        $this->x = $x;
        $this->y = $y;
    }
}

$p1 = new Point(4, 5);

Java with overloading (multiple constructors)

public class MyClass {

    private int number = 0;

    public MyClass() {
    }

    public MyClass(int theNumber) {
        this.number = theNumber;
    }
}

Solidity

pragma solidity ^0.5.0;

contract Base {
   uint data;

   constructor(uint _data) public {
      data = _data;
   }
}

What about Go?

Go language doesn't enforce constructors. It constructs structs as "composite literals".

Composite literals construct values for structs, arrays, slices, and maps and create a new value each time they are evaluated.
They consist of the type of the literal followed by a brace-bound list of elements. A corresponding key may optionally precede each element.

Composite literal example:

type Point3D struct { x, y, z float64 }
type Line struct { p, q Point3D }

Can be initialized as:

origin := Point3D{}                            // zero value for Point3D
line := Line{origin, Point3D{y: -4, z: 12.3}}  // zero value for line.q.x

"As a limiting case, if a composite literal contains no fields at all, it creates a zero value for the type. The expressions new(Point3D) and &Point3D{} are equivalent."

You can remember it as, COMPOSING TOGETHER VALUES of different TYPES with DEFAULT VALUES if no arguments are supplied. Type + Curly Brackets.

Go is designed with simplicity and boilerplate-free code in mind and composite literals are precisely that, but they are unnecessarily difficult to maintain.

Why? Let me unpack my day-to-day problems with composite literals and how I like to solve them.

Create your constructor to keep the Go codebase maintainable

My constructor rule:

When creating a new struct, like below, Transaction, Tx, I immediately create a dedicated function constructor: NewTx().

type Tx struct {
	From     common.Address `json:"from"`
	To       common.Address `json:"to"`
	Gas      uint           `json:"gas"`
	GasPrice uint           `json:"gasPrice"`
	Value    uint           `json:"value"`
	Nonce    uint           `json:"nonce"`
	Data     string         `json:"data"`
	Time     uint64         `json:"time"`
}

func NewBaseTx(from, to common.Address, value uint, nonce uint, data string) Tx {
	return Tx{from, to, TxGas, TxGasPriceDefault, value, nonce, data, uint64(time.Now().Unix())}
}

Let me walk you through my four reasons for doing this. I believe you will find it helpful.

Reason 1 - Sensible default values are rare

No constructor

Let's say I don't create a dedicated constructor NewBaseTx(). If I had to add another attribute to the Tx struct, MaxGasPrice, I would have to modify tens/hundreds of files (depending on the codebase size/implementation). I am not exaggerating here. That's a very realistic estimate as I have already been in several such situations on various projects. too few constructor values

Field:value constructor

However, if I would label the constructor elements "explicitly as field:value pairs, the initializers can appear in any order, with the missing ones left as their respective zero values", the refactoring could quickly end up with a production bug, as the compiler wouldn't point out all the places I forgot to pass a new value; so that's even worse approach.

MaxGasPrice will accidentally be 0 by default, turning the struct into an invalid state. Don't even try to fix it with an optional setter. That's pure evil. field:value zero

Generally, I don't see any value in the field:value constructors given any good IDE will graphically show you the names like my previous screenshot. What's your experience? Agree or do you think differently?

Dedicated constructor

Fortunately, I can avoid all that mess, and a large PR for my teammates, because I have control over the struct creation. I encapsulated this responsibility into a single function. I needed to change only one place. constructor values

Every struct deserves its constructor! Your future self will thank you during the next refactoring session :)

To be objective, one struct has default values that work like a charm, and I frequently use: sync.RWMutex.

type Mutex struct {
	state int32
	sema  uint32
}

type RWMutex struct {
	w           Mutex  // held if there are pending writers
	writerSem   uint32 // semaphore for writers to wait for completing readers
	readerSem   uint32 // semaphore for readers to wait for completing writers
	readerCount int32  // number of pending readers
	readerWait  int32  // number of departing readers
}

rwm := sync.RWMutex{}
rwm.Lock()

Very elegant. But. I have been writing Go for 4 years, and I can't pinpoint a single struct I designed using default values solely. However, I can link to 3 PRs in the last 6 months that caused a production bug because of a missing dedicated constructor. A wrong default value was supplied as the Go compiler can't pick it up; given from a compiler perspective, the value is not missing; it thinks you didn't pass it because you wanted the compiler to use a default value.

Reason 2 - Your code changes weekly, RWMutex doesn't

The last commit to Mutex source was in November 2019. Today is October 2021. last commit mutex

I don't know about you, but the latest commit in my project was like 3h ago, lol.

Point being? It would be best if you optimized for flexibility and refactoring as a team in your project. Dedicated constructors help with that.

Reason 3 - Exported Structs and Public Libraries

Public Structs and Libraries are even more sensitive to changes. Last month I created a bug due to a missing constructor. It all came down like this...

A Config struct was initialized with field:value syntax, and I added a new attribute without a possible "sensible default."

Config{
	IsObserverEnabled: func() bool {
		return false
	},
	IsFilteringEnabled: func() bool {
		return false
	},
}

I reviewed the entire codebase, adjusted all usages, updated tests, and made a new release version.

So far, all good. Except, when I imported the library to 3 microservices using it, I overlooked one usage, and a wrong default value sneaked in and messed up a service deployment. If only there were a dedicated NewConfig() constructor, the compiler would throw an error after importing the library. I would also not have to change 3 microservices, just the one NewConfig() function in the library!

Reason 4 - Breaking Single Responsibility Principle (SRP)

It seems like because of the lack of boiler-plate, when developers do create the dedicated constructor, they hijack it with extra logic that doesn't belong there like Goroutines, disk operations, DB connections.

func NewDatabase(cfg Config) (*DB, error) {
	ctx, cancel := context.WithCancel(context.Background())

	db := &DB{
		// stuff
	}

	go db.connect(ctx)

	return db
}

Polluting constructors make it challenging to maintain, test and use.

A constructor should only create a new instance of the object.

That's it. It's been like this for decades, and this principle served us well. When I call: NewDatabase(cfg) on some library, the last thing I expect is a DB network connection firing up inside a goroutine behind the scenes.

You can't test it, and you can't even initialize the struct before a DB connection is even possible; it's just a terrible practice. This logic belongs to a dedicated Connect() function, not a constructor. Two different responsibilities.

This has been my experience with Go and constructors so far.

What's yours? Let me know on Twitter:

Get Started with Blockchain Development

Hi, I am writing an eBook on how to build a peer-to-peer system in Go from scratch!

Follow how the main protagonist Andrej, a web developer by day and a bartender by night, learns real-world use of blockchain through the growth of his new business venture: "The Blockchain Bar".

Free chapter