Today I'm discussing a trivially simple technique that I've rarely seen used in production codebases.
In programming, we often need to deal with simple values that can be represented by simple, generic types built into our programming language or provided by libraries: types like integer, string, or UUID.
In any nontrivial codebase, this inevitably leads to bugs when, for example, a string representing a user ID gets used as an account ID, or when a critical function accepts three integer arguments and someone mixes up the correct order when calling it.
A much better solution is to define different types and use them when representing different things! int or string are excellent building blocks, but passing them around your system as-is means you slowly but inevitably lose important context: what they actually represent.
Example
Here's a trivial example of what that might look like. Imagine if, instead of using plain old UUIDs, each of your models defined its own ID type:
type AccountID uuid.UUID type UserID uuid.UUID func UUIDTypeMixup() { { userID := UserID(uuid.New()) DeleteUser(userID) // no error } { accountID := AccountID(uuid.New()) DeleteUser(accountID) // ^ error: Cannot use 'accountID' (type AccountID) as the type UserID } { accountID := uuid.New() DeleteUserUntyped(accountID) // no error at compile time; likely error at runtime } }
In libwx
I previously discussed this in my 2015 talk, "String is not a sufficient type." I've found a great demonstration case for the technique in my weather & atmospheric calculations library for Golang, libwx . This library defines types for every measurement it deals with, along with methods for converting between the different types (for example, Km.Miles() ).
This prevents the user from making mistakes that would be all too easy if the library dealt entirely in float64 s:
... continue reading