short vs. long lifetime structs
Short vs. Long lifetime structs
Much of the go code that we write the structure of; do some operation that might fail, return value and error, handle error if present, continue with value. Here’s an example:
|
|
This is a relatively simple example, but I will show how with multiple method calls which return errors, the intent of the original function can easily be confused or lost in the noise of error handling.
Dave Cheney’s article “Eliminate error handling by eliminating errors” demonstrates how using errors as values we can reduce the number of places error handling needs to be performed. We can extend on this idea by thinking in-terms of two different classifications of objects; long lived and short lived structs.
In the example above the Service
struct is an instance of a long lived struct. It’s stateless and thread-safe and this pattern is commonly used for; controllers, service, clients etc. These long lived structs have fields that carry dependencies and interfaces, and data is passed to the struct instance via method arguments.
If we look at a more complicated example, this code reticulates some splines based on the diameter
passed.
|
|
In this example, the multiple instances of error handling hide the intent of the method. Let’s consider a slightly rewritten version:
|
|
Why is the second version better?
- readability; the
reticulater
provides methods that abstract away the noise of error handling, making the code’s intent much clearer. - testing; the
reticulater
struct carries data and not interfaces which allows the struct to be setup with specific data once, and the various code paths to be exercised using different setup and expectations on mocks that are passed as interfaces (the “thing that changes”).
What is it about go that makes the use of ‘short lived structs’ easy?
- Defining a new struct type has a low overhead. When compared to languages like java, the overhead of defining a new struct type is much lower. There’s less boilerplate required and the short lived struct can easily live in the same file as the long lived one.
- Short lived struct types can define their own interfaces on the dependencies that they accept to reduce the testing surface area.
Rules:
If we follow some rules, we can make the best use of short lived structs to simplify the intent of our code.
Pass the dependency, store the data.
Passing the dependency has the benefit of making it clear that this object is a short lived behaviour / data object and not just another stateless service struct (like the ones that have caused us the repetitive
if err
checks). Passing the dependency also allows the data struct to act as an abstraction, in thedata.persist(repo)
example the data struct can choose if it wants to operate against the repo or not, this is hidden from the consumer.Carry the error and check it first.
Making the error a value like all the other data in the struct means that we can carry the error on the struct, and our final method can be responsible for checking and returning the error. This groups all the error handling from all the setup methods into a single
if err
check.
TL;DR
Long lived service structs are stateless and thread-safe implementations of controller, service or client logic. Their fields comprise the dependencies and interfaces interacted with (often external, e.g. a repository or database).
Sort lived data structs define behaviours and carry data. The dependencies and interfaces they need are passed in with method calls, this makes them easier to test. Errors are treated as data values of the struct to simplify the error handling. The behaviour methods provide readable abstractions.
Appendix:
I’ve excluded from the example above the method implementations for the reticulater
, here they are:
|
|