Type alias / declaration abuse in Go

The power of habits

Most of us while switching to a new programming language try to bring habits and ideas from technologies we used before. There are applicable ideas and ideas that conflict with how the new language is designed and supposed to be used.

When you try to bring ideas from second category problems arise. You make the code look like what you used to in last years. You try to make solutions look similar to how you would do it in language that you have used before.

When that happens, other people in the team will start to complain about you making code hard to read. They will complain that the API you have designed is hard to use. And that it is not the "language way" to do things.

It just happened that I am the person who will start complain first as soon as I would see that person tries to do things, not in the way things supposed to be done.

This post is the read for people who switched to Golang and specifically who are trying to bring super - duper type safety to the language.

I want to show and discuss a real example of how someone can misinterpret language feature and start abusing it to achieve some calmness of willing to bring his/her previous experience.

But first let's list essential ideas, principles, best practices that you can use in almost any language.

Essential ideas / principles / good practices

There are essential ideas and principles that can be used universally in any language or technology:

How can we describe good code? Code which:

How can you archive that?

Most problems in the code arise because of people laziness. Quite often what happens is, while trying to implement a new feature or trying to fix a specific bug, you see that in the ideal case you have to change the design a little bit, but on another hand, you can add horrible hack and feature ready. You will have abstraction leak, not consistent API. But the task in JIRA can be closed. And you do it.

It may seem like you move fast doing dirty hacks, that you deliver as crazy. But in reality, you increase your tech dept, and quite often you will pay twice later. And I want to make it clear here, that we do not talk about "IDEAL" solutions here. If it was at least 1/10 from ideal that would be nice.

Making good software means that you value good engineering and work to improve your habits so that with time, solutions you generate on the fly are good by default. It requires strong discipline and hard work on improving yourself.

Golang

I love Golang for being so pragmatic and simple tool. It has minimal syntax, standard formatting and tooling. The projects on Github are written almost in the same style, problems solved in the same manner. It is easy to follow and to write.

But even having all that, so people will try to invent something.

Let's talks a little bit about golang type declaration syntax. We need some introduction before going further.

Golang type declaration syntax.

A type declaration binds an identifier, the type name, to a type. Type declaration comes in two forms:

An alias declaration has the form

type T1 = T2

as opposed to a standard type definition

type T1 T2

An alias declaration doesn't create a new distinct type different from the type it's created from. It just introduces an alias name T1, an alternate spelling, for the type denoted by T2.

More examples:

type Node struct { // Type definition
  value interface{}
  Left *Node
  Right *Node
}
type MyNode = Node // Type alias

When is it supposed to be used?

Type aliases are not meant for everyday use. They were introduced to support gradual code repair while moving a type between packages during large scale refactoring. And should be used only for that purpose. Codebase Refactoring (with help from Go)

Type definitions meant to be used of course to express your problem domain. The way those elements of domain interact with each other.

Distinct type for basic types

What do I want to talk about is the case when you do want to create a new distinct type for basic golang types like byte, int, string, slice, map etc.

There are two cases when you do want to create a new type for primitive type:

1. Constants using iota

Constant type of Vehicle.

type Vehicle int

const (
    Bike Vehicle = iota
    Scooter 
    Car
    Bus
    Train
)

Here it really makes sense, because we created a new type and we limited value range. Using it everywhere after makes code more type safe.

2. Additional behaviour

In rare cases when you want to add behaviour to basic types, you can create a new type with method:

type Person string

func (p Person) Hello(anotherPerson Person) {
    fmt.Printf("%s, Hello from %s", anotherPerson, p)
}

elon := Person("Elon Musk")
aleh := Person("Aleh")

elon.Hello(aleh) // Aleh, Hello from Elon Musk

How to abuse type declaration?

In one of the projects I've participated, I found code similar to the code below. I am not going to judge how to type-safe is it, send me a message and tell what do you think.

We have User struct like this:

package app

type User {
    Firstname Firstname
    Lastname  Lastname
    Username  Username
    Password  Password
    Email     Email
    DOB       DOB
}

How readable is it? Double readable! Let's check what do those types mean:

package app

type Firstname string
type Lastname string
type Username string
type Password string
type Email string
type DOB time.Time

Sometimes you can also find code with type aliases instead of type declarations:

type Firstname = string
type Lastname = string
type Username = string
type Password = string
type Email = string
type DOB = time.Time

Why?

What drives people to do that?

Let's suppose we have the function doMagic:

func doMagic(firstname Firstname, lastname Lastname, username Username, email Email) MagicResult {}

You use it like that:

doMagic(user.Firstname, user.Lastname, user.Username, user.Email) // cool
doMagic(user.Lastname, user.Firstname, user.Username, user.Email) // type error

// Now suppose that you don't have user struct, but have string values:
doMagic(app.Firstname("Ilon"), app.Lastname("Musk"), app.Username("spacemusk"))

What prevents you from passing any random string with those type casts? Nothing.

// no errors, but logic error present
doMagic(app.Firstname("Musk"), app.Lastname("Ilon"), app.Username("spacemusk")) 

Want to have additional documentation, create additional struct like:

type MagicArgs struct {
  Firstname string
  Lastname string
  Username string
}

And use it passing to function doMagic.

doMagic(MagicArgs{
    Firstname: "Ilon",
    Lastname: "Musk",
    Username: "spacemusk",
})

It will prevent you from passing the wrong attribute to the function parameter. How often do you make such mistakes?

Problems

The problem here is that when you create new type distinct type from primitive, you can't use it anymore like primitive. If you have a function like:

func deleteByUsername(username string) (*User, error) { ... }

user := getUser() // return User
deleteByUsername(user.Username) // error Username is not a string
deleteByUsername(string(user.Username)) // OK now

If you want to have Fullname function on User struct:

func (u *User) Fullname() {
  return string(u.Firstname) + " " + string(u.Lastname)
} 

So each time you want to use a real type of that field you have to cast.

Want to call the format function on DOB? You have to cast it first to time.Time and after that, you will be able to use it.

dob := user.DOB
dobTime := time.Time(dob)
dobTime.Format(time.UnixDate)

Cool right? To use time. The time you have to cast it first to time.Time.

The funny story happens when developers create two different types for the same thing in the same project but in different packages. Let's imagine that we have functionality in another package where someone defined the function:

package report

func getUserReport(username Username) (*Report, error) { }

And suppose we have User struct from package app

package another

user := getCurrentUser() // returns app.User

report.getUserReport(user.Username) // error app.Username != report.Username

report.getUserReport(report.Username(user.Useranme)) // and you have to use it like that. What a type safety!

How helpful that we have types? In my opinion not very helpful.

In some projects you can find also such types:

type Entity string
type Entities []Entity
type ArrayOfEntities []Entities
type ArrayOfArrayOfEntities []ArrayOfEntities
type ArrayOfArrayOfArrayOfEntities []ArrayOfArrayOfEntities

What would you prefer [][][][]string or ArrayOfArrayOfArrayOfEntities?

Joke

type Aleh string
type (a Aleh) Goodbye() {
    fmt.Println("Goodbye from" + string(a))
}

var aleh Aleh
aleh = "Aleh"
aleh.Goodbye() // Goodbye from Aleh

Conclusion

Create good software by using essential ideas, principles, best practices. Write code which is well designed, pleasant to read, tests and documentation. Then you will fill confident with types or without. You don't need to invent any super duper type-safety to create resilient software. Good luck!

Links