Introducing Golistics
Golistics is a linter that’ll help Go developers to keep type-safe and reflectionless “holistic” methods like serializer, validator and comparators up-to-date and complete, even when developers miss to update after changing the field list.
Problem
Let me introduce you the problem.
Instead of using reflection, I usually implement manual methods that needs to visit all fields of a struct. Like validators, serializers or comparators. Eg:
type Request struct {
Age HumanAge
School School
Class StudentClass
}
func (r Request) Validate() map[string]error {
errs := map[string]error{}
if err := r.Age.Validate(); err != nil {
errs["age"] = err
}
// ...
}
func (r Request) String() string {
return fmt.Sprintf(`Age: %q, School: %q, Class: %q`, r.Age, r.School, r.Class)
}
func (r Request) Equals(s Request) bool {
return r.Age == s.Age &&
r.School == s.School &&
r.Class == s.Class
}
Note that the Request.String and Request.Equals is accessing all the fields by principle. Which means they also need to preserve that propery throughout the future changes made in the field list of Request type.
The problem is that each such struct adds ~3 new long-term liability to my chore list because each such method needs to be updated with the list of fields of the struct. Adding the 4th field to Request creates a need navigate to the struct’s methods and check if anyone needs update.
I thought why it would not be better to use linter to perform those checks at CI if any such method needs an update?
Solution
Now let me introduce you my solution.
I created a program I call Golistics, honoring the use of word “holistic” as if it is a known term to describe the methods that supposed to access all fields of a struct, or they get outdated.
Golistics is a Go-vet style analyzer (or a linter-plugin in a broad sense) which warns the developer when a desired method goes out-of-sync with the receiver’s (or underlying type’s) field list. You opt-in any method for such checks by adding this annotation to the method doc-comment:
//golistics:all
func (r Request) String() string
Note, method doc-comment is the comment group above the method’s signature.
Partial holisticallity is also supported. As well as checking all fields, Golistics can also be directed to only require exported fields:
//golistics:exported
func (r Request) String() string
Install
Installing Golistics is easy. See the repository for up-to-date instructions.
Example
Briefly:
type (
Borders struct {
Top, Right, Bottom, Left Border
}
Margin struct {
Top, Right, Bottom, Left any
}
Dimensions struct {
Height any
Width any
unexported any
}
)
//golistics:exported
func (s Dimensions) Strings() []string {
return nil
}
//golistics:all
func (s Borders) IsEqual(y Borders) bool {
return false
}
//golistics:all
func (s Margin) IsEqual(y Margin) bool {
return safeEq(s.Right, y.Right) &&
safeEq(s.Bottom, y.Bottom) &&
safeEq(s.Left, y.Left)
}
Run the linter:
$ golistics ./...
gss.go:173:1: missing fields: Height, Width
gss.go:241:1: missing fields: Bottom, Left, Right, Top
gss.go:246:1: missing field: Top
Testing
The solution is both unit and manually tested.
The test case for unit test can be seen on GitHub with // want:-like assertions. Maybe it communicates the problem better than the short example above.
Summary
Golistics reduces the duration for noticing outdated holistic methods and possbility to ship code with them; thus improving reliability and maintainability.